From 5408d711b143531e2f6931ce1675174e1e72101a Mon Sep 17 00:00:00 2001 From: iisaev Date: Thu, 10 Sep 2026 10:59:37 +0300 Subject: [PATCH] init --- .gitignore | 37 + dll/Live2D.deps.json | 192 +++++ dll/Live2D.dll | Bin 0 -> 75776 bytes dll/Live2D.pdb | Bin 0 -> 47428 bytes manifest.json | 10 + mod.png | Bin 0 -> 2027 bytes native/Live2DCubismCore.dll | Bin 0 -> 249800 bytes source_code/Cubism/Core/CubismCoreBindings.cs | 206 ++++++ source_code/Cubism/Framework/ACubismMotion.cs | 182 +++++ source_code/Cubism/Framework/CubismBreath.cs | 81 +++ .../Framework/CubismExpressionMotion.cs | 125 ++++ .../Cubism/Framework/CubismEyeBlink.cs | 136 ++++ source_code/Cubism/Framework/CubismMath.cs | 202 ++++++ .../Cubism/Framework/CubismModelMatrix.cs | 175 +++++ .../Cubism/Framework/CubismModelNode.cs | 520 ++++++++++++++ .../Framework/CubismModelSettingJson.cs | 239 +++++++ source_code/Cubism/Framework/CubismMotion.cs | 439 ++++++++++++ .../Cubism/Framework/CubismMotionInternal.cs | 62 ++ .../Cubism/Framework/CubismMotionJson.cs | 138 ++++ .../Framework/CubismMotionQueueEntry.cs | 163 +++++ .../Cubism/Framework/CubismNativeModel.cs | 334 +++++++++ .../Cubism/Framework/CubismParameter.cs | 50 ++ .../Cubism/Framework/CubismPartOpacity.cs | 43 ++ source_code/Cubism/Framework/CubismPhysics.cs | 365 ++++++++++ .../Cubism/Framework/CubismPhysicsJson.cs | 240 +++++++ source_code/Cubism/Framework/CubismPose.cs | 218 ++++++ .../Cubism/Framework/CubismRenderer2D.cs | 661 ++++++++++++++++++ .../Cubism/Framework/CubismTargetPoint.cs | 100 +++ source_code/Cubism/Handlers/Live2DService.cs | 85 +++ source_code/Live2D.csproj | 14 + source_code/build.bat | 37 + src/AddLive2DModelAction.cs | 62 ++ 32 files changed, 5116 insertions(+) create mode 100644 .gitignore create mode 100644 dll/Live2D.deps.json create mode 100644 dll/Live2D.dll create mode 100644 dll/Live2D.pdb create mode 100644 manifest.json create mode 100644 mod.png create mode 100644 native/Live2DCubismCore.dll create mode 100644 source_code/Cubism/Core/CubismCoreBindings.cs create mode 100644 source_code/Cubism/Framework/ACubismMotion.cs create mode 100644 source_code/Cubism/Framework/CubismBreath.cs create mode 100644 source_code/Cubism/Framework/CubismExpressionMotion.cs create mode 100644 source_code/Cubism/Framework/CubismEyeBlink.cs create mode 100644 source_code/Cubism/Framework/CubismMath.cs create mode 100644 source_code/Cubism/Framework/CubismModelMatrix.cs create mode 100644 source_code/Cubism/Framework/CubismModelNode.cs create mode 100644 source_code/Cubism/Framework/CubismModelSettingJson.cs create mode 100644 source_code/Cubism/Framework/CubismMotion.cs create mode 100644 source_code/Cubism/Framework/CubismMotionInternal.cs create mode 100644 source_code/Cubism/Framework/CubismMotionJson.cs create mode 100644 source_code/Cubism/Framework/CubismMotionQueueEntry.cs create mode 100644 source_code/Cubism/Framework/CubismNativeModel.cs create mode 100644 source_code/Cubism/Framework/CubismParameter.cs create mode 100644 source_code/Cubism/Framework/CubismPartOpacity.cs create mode 100644 source_code/Cubism/Framework/CubismPhysics.cs create mode 100644 source_code/Cubism/Framework/CubismPhysicsJson.cs create mode 100644 source_code/Cubism/Framework/CubismPose.cs create mode 100644 source_code/Cubism/Framework/CubismRenderer2D.cs create mode 100644 source_code/Cubism/Framework/CubismTargetPoint.cs create mode 100644 source_code/Cubism/Handlers/Live2DService.cs create mode 100644 source_code/Live2D.csproj create mode 100644 source_code/build.bat create mode 100644 src/AddLive2DModelAction.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c301ca8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Godot 4.x +.godot/ +.idea/ +sdk/ +bin/ +obj/ + +#ai +.omo/ +.opencode/ + +gen/ +obj/ +bin/ + +*.tres~ +*.tscn~ + +# GDScript LSP +.godot/ + +# Export +*.exe +*.dmg +*.apk +*.aab +*.pck +*.zip +*.log + +# OS +.DS_Store +Thumbs.db + +# Editor +*.import +*.godot.uid diff --git a/dll/Live2D.deps.json b/dll/Live2D.deps.json new file mode 100644 index 0000000..7a4e42a --- /dev/null +++ b/dll/Live2D.deps.json @@ -0,0 +1,192 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v8.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v8.0": { + "Live2D/1.0.0": { + "dependencies": { + "Cthangover": "1.0.0" + }, + "runtime": { + "Live2D.dll": {} + } + }, + "GodotSharp/4.6.3": { + "runtime": { + "lib/net8.0/GodotSharp.dll": { + "assemblyVersion": "4.6.3.0", + "fileVersion": "4.6.3.0" + } + } + }, + "GodotSharpEditor/4.6.3": { + "dependencies": { + "GodotSharp": "4.6.3" + }, + "runtime": { + "lib/net8.0/GodotSharpEditor.dll": { + "assemblyVersion": "4.6.3.0", + "fileVersion": "4.6.3.0" + } + } + }, + "Microsoft.CodeAnalysis.Common/4.11.0": { + "runtime": { + "lib/net8.0/Microsoft.CodeAnalysis.dll": { + "assemblyVersion": "4.11.0.0", + "fileVersion": "4.1100.24.37604" + } + }, + "resources": { + "lib/net8.0/cs/Microsoft.CodeAnalysis.resources.dll": { + "locale": "cs" + }, + "lib/net8.0/de/Microsoft.CodeAnalysis.resources.dll": { + "locale": "de" + }, + "lib/net8.0/es/Microsoft.CodeAnalysis.resources.dll": { + "locale": "es" + }, + "lib/net8.0/fr/Microsoft.CodeAnalysis.resources.dll": { + "locale": "fr" + }, + "lib/net8.0/it/Microsoft.CodeAnalysis.resources.dll": { + "locale": "it" + }, + "lib/net8.0/ja/Microsoft.CodeAnalysis.resources.dll": { + "locale": "ja" + }, + "lib/net8.0/ko/Microsoft.CodeAnalysis.resources.dll": { + "locale": "ko" + }, + "lib/net8.0/pl/Microsoft.CodeAnalysis.resources.dll": { + "locale": "pl" + }, + "lib/net8.0/pt-BR/Microsoft.CodeAnalysis.resources.dll": { + "locale": "pt-BR" + }, + "lib/net8.0/ru/Microsoft.CodeAnalysis.resources.dll": { + "locale": "ru" + }, + "lib/net8.0/tr/Microsoft.CodeAnalysis.resources.dll": { + "locale": "tr" + }, + "lib/net8.0/zh-Hans/Microsoft.CodeAnalysis.resources.dll": { + "locale": "zh-Hans" + }, + "lib/net8.0/zh-Hant/Microsoft.CodeAnalysis.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "Microsoft.CodeAnalysis.CSharp/4.11.0": { + "dependencies": { + "Microsoft.CodeAnalysis.Common": "4.11.0" + }, + "runtime": { + "lib/net8.0/Microsoft.CodeAnalysis.CSharp.dll": { + "assemblyVersion": "4.11.0.0", + "fileVersion": "4.1100.24.37604" + } + }, + "resources": { + "lib/net8.0/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "cs" + }, + "lib/net8.0/de/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "de" + }, + "lib/net8.0/es/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "es" + }, + "lib/net8.0/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "fr" + }, + "lib/net8.0/it/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "it" + }, + "lib/net8.0/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "ja" + }, + "lib/net8.0/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "ko" + }, + "lib/net8.0/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "pl" + }, + "lib/net8.0/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "pt-BR" + }, + "lib/net8.0/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "ru" + }, + "lib/net8.0/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "tr" + }, + "lib/net8.0/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "zh-Hans" + }, + "lib/net8.0/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "Cthangover/1.0.0": { + "dependencies": { + "GodotSharp": "4.6.3", + "GodotSharpEditor": "4.6.3", + "Microsoft.CodeAnalysis.CSharp": "4.11.0" + }, + "runtime": { + "Cthangover.dll": { + "assemblyVersion": "0.0.0.0", + "fileVersion": "0.0.0.0" + } + } + } + } + }, + "libraries": { + "Live2D/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "GodotSharp/4.6.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-2gLsonAxGdDGm9/yYDy4A1U+db8JFGvlzt6ZuzX14cqrkCa7noikZDHcqtujcVAjXLOyxm+TuMdsmihYLZq1RA==", + "path": "godotsharp/4.6.3", + "hashPath": "godotsharp.4.6.3.nupkg.sha512" + }, + "GodotSharpEditor/4.6.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ytyJqJVpB5/7ZXKO9ahKzK24wlengf1FdNFHoIopa72irgon/HpizIcuzh/9lfGytfZywKi97o/LIQF9j7eHNA==", + "path": "godotsharpeditor/4.6.3", + "hashPath": "godotsharpeditor.4.6.3.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.Common/4.11.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-djf8ujmqYImFgB04UGtcsEhHrzVqzHowS+EEl/Yunc5LdrYrZhGBWUTXoCF0NzYXJxtfuD+UVQarWpvrNc94Qg==", + "path": "microsoft.codeanalysis.common/4.11.0", + "hashPath": "microsoft.codeanalysis.common.4.11.0.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.CSharp/4.11.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==", + "path": "microsoft.codeanalysis.csharp/4.11.0", + "hashPath": "microsoft.codeanalysis.csharp.4.11.0.nupkg.sha512" + }, + "Cthangover/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/dll/Live2D.dll b/dll/Live2D.dll new file mode 100644 index 0000000000000000000000000000000000000000..40bc7e9ab4096e667be25ef53064ca832210dc22 GIT binary patch literal 75776 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 z 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 literal 0 HcmV?d00001 diff --git a/dll/Live2D.pdb b/dll/Live2D.pdb new file mode 100644 index 0000000000000000000000000000000000000000..fc6edba1bcd7ed84f6bbec2f9b29f453025930bf GIT binary patch literal 47428 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

q3D;;yK?i^*$3RUMqtDhQXmWHcmz+#Z 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 OiE5K=1"], + "depends_native": ["native/*.dll"], + "sources": ["src/*.cs"] +} diff --git a/mod.png b/mod.png new file mode 100644 index 0000000000000000000000000000000000000000..d112f2e1b8139b0d6f27513f6e6b6f4819419956 GIT binary patch literal 2027 zcmZ8hdpOhkAOCK)%+!n=$0^l_WE)0HWHiiWVWAzDQ7*Yp5)u)kw&PN?jxMIOlP)`* za>;Rbw5D=Ph$2Np!VI|_SuK8Fo%8$We4gj^dA;8E=lQ&!=ac0|bKHR3gaiO!1J#M* z0Wxb12nFz76mw-DLE=0dZGn;(T9cpw3$t;x0f6#6WqBYRjBRmqq1$6I*lc!oeqMgU zu>`>_LEo!Bkx-P9kYcQ7?C<68b->HT&c)Kya$$a9b#=9^xvjRUmSRbn7@w#pt0>|X zMTJC}?lkRa@A#|sFT?GI$C<|^5{XG9mXg{u%fp^4WKLq31Hn=;kxCd^ya}->(1@`YqysIIW zxEhT&NHC*O2==>?(-aG!_vC#OFfw3WgTVOCia4L(Nu~JJ@N1~FT!pU70qcrW5*4(GK&;$ z-=ybosBd>xbH+0qN5Bdr*{-a1_Dg221k$di&lG$UF0gq)b&~7%ib7H6tWPu6uScOC zOVfwXo)1Q@EIQ?MKi6z9dw6avV2PY-v26FHxm;<-4#(T?Gj>2+F2B-dX{x6x4?zXD z4$9+L64~8Pn$HR+DM1drieq8(Z29wR?{qvCJT-Q9-~1E@nGN<;b58m#z$c|!J6>o+#j)-*)s2>IGzKb@c3i>CWQbGKovUO0zn2jug(w{fD?`8h!c)t3CVmSSRVv zbvOD$?(8{Jf@jiHLy?Lq+z+GKsgG*i9jE*><|+(oJX1u z+$|(IHn&vBe6E^ZgD%%yWN%1;x5O3gdX{&kpndB;L_u1%_1LiV*D=2la}=|p+A~@tc4dO7Ws;l)= zt)Zs%W_wW;2X0uOUh&Fr`=u;scw%a<)j&F7VY!-M`$UgVZxm0CstK7YL1@;%nHKof zN<~%Wvfix}1F=+Y`H+!GBR_RdMqdI>PTS~0NlXy+0^(?<(h^%2kfy$q@e0| zt1bin>FIQ9$n{7SqLSb>jmzK2fYBuo&_p1-llcttrvZgZsHk_VKpkDOL@r=;RVvrZWB;?ghow2*Ncx!o`Et{3QuXfr^TR0wRtcitT=w5#N zFiyr8&mZLiFLbd!+K_m+605U_7-5fok*GB#D{42zxO;jev-po?si~abBFX(Mtu!l< zc?c#JW7MX1&I1voC@vfsP@v2{nJba~4t~QWbZt-@ki8OCtFrPnNX#^!o}X2Ncgv>TzxI87 z|K%fWKEp0=V4IcosIMVTEhw&Nsye1h@H(n*Ik$f@2H zY3q2&D<&^qC2!*A!w%!eXAPmN{5Yy8U`jf8KqG-2q=v?tqzu~S>G01GOib4r+zZ@#6NkNeC%Pxk2V aw`6`uMGs| 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 + /// P/Invoke bindings for the Live2D CubismCore native library (Live2DCubismCore.dll). + /// Provides direct access to model loading, updating, and data querying at the C level. + /// + /// All methods are unsafe and return raw pointers into CubismCore's internal arrays. + /// wraps this layer into a managed API with proper + /// memory alignment and lifetime management. + /// + public static class CubismCoreBindings + { + ///

Name of the native DLL loaded at runtime. + public const string DllName = "Live2DCubismCore"; + + /// Required byte alignment for .moc3 data buffers before revival. + public const int AlignofMoc = 64; + + /// Required byte alignment for model instance memory. + public const int AlignofModel = 16; + + /// Flag: drawable uses additive blending. + public const byte BlendAdditive = 1 << 0; + + /// Flag: drawable uses multiplicative blending. + public const byte BlendMultiplicative = 1 << 1; + + /// Flag: drawable is double-sided (no back-face culling). + public const byte IsDoubleSided = 1 << 2; + + /// Flag: drawable's mask is inverted. + public const byte IsInvertedMask = 1 << 3; + + /// Dynamic flag: drawable is currently visible. + public const byte IsVisible = 1 << 0; + + /// Dynamic flag: drawable visibility changed this frame. + public const byte VisibilityDidChange = 1 << 1; + + /// Dynamic flag: drawable opacity changed this frame. + public const byte OpacityDidChange = 1 << 2; + + /// Dynamic flag: drawable draw order changed this frame. + public const byte DrawOrderDidChange = 1 << 3; + + /// Dynamic flag: drawable render order changed this frame. + public const byte RenderOrderDidChange = 1 << 4; + + /// Dynamic flag: vertex positions changed this frame. + public const byte VertexPositionsDidChange = 1 << 5; + + /// Dynamic flag: blend color changed this frame. + public const byte BlendColorDidChange = 1 << 6; + + /// 2D vector used in CubismCore struct layouts. + [StructLayout(LayoutKind.Sequential)] + public struct csmVector2 { public float X; public float Y; } + + /// 4D vector used in CubismCore color struct layouts. + [StructLayout(LayoutKind.Sequential)] + public struct csmVector4 { public float X; public float Y; public float Z; public float W; } + + /// Returns the CubismCore SDK version number. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern uint csmGetVersion(); + + /// Revives a .moc3 binary from an aligned memory buffer. Returns a moc handle or IntPtr.Zero. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe IntPtr csmReviveMocInPlace(void* address, uint size); + + /// Queries the memory size needed to initialize a model from the given moc. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe uint csmGetSizeofModel(IntPtr moc); + + /// Creates a model instance inside the provided aligned buffer. Returns model handle or IntPtr.Zero. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe IntPtr csmInitializeModelInPlace(IntPtr moc, void* address, uint size); + + /// Advance the model by one frame — updates physics, parameter-to-vertex mapping, etc. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe void csmUpdateModel(IntPtr model); + + /// Reads the model's canvas dimensions and pixels-per-unit scale. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe void csmReadCanvasInfo(IntPtr model, csmVector2* outSizeInPixels, csmVector2* outOriginInPixels, float* outPixelsPerUnit); + + /// Returns the number of parameters in the model. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int csmGetParameterCount(IntPtr model); + + /// Returns a pointer array of null-terminated parameter ID strings. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe IntPtr csmGetParameterIds(IntPtr model); + + /// Returns a float array of current parameter values. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe float* csmGetParameterValues(IntPtr model); + + /// Returns a float array of parameter minimum bounds. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe float* csmGetParameterMinimumValues(IntPtr model); + + /// Returns a float array of parameter maximum bounds. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe float* csmGetParameterMaximumValues(IntPtr model); + + /// Returns a float array of parameter default values. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe float* csmGetParameterDefaultValues(IntPtr model); + + /// Returns the number of drawables (renderable mesh parts) in the model. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int csmGetDrawableCount(IntPtr model); + + /// Returns byte flags array for constant (immutable) drawable properties. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe byte* csmGetDrawableConstantFlags(IntPtr model); + + /// Returns byte flags array for per-frame dynamic drawable state. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe byte* csmGetDrawableDynamicFlags(IntPtr model); + + /// Returns an int array mapping each drawable to a texture index. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int* csmGetDrawableTextureIndices(IntPtr model); + + /// Returns an int array of vertex counts per drawable. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int* csmGetDrawableVertexCounts(IntPtr model); + + /// Returns a pointer array of csmVector2 arrays (vertex positions per drawable). + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe csmVector2** csmGetDrawableVertexPositions(IntPtr model); + + /// Returns a pointer array of csmVector2 arrays (UVs per drawable). + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe csmVector2** csmGetDrawableVertexUvs(IntPtr model); + + /// Returns an int array of index counts per drawable. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int* csmGetDrawableIndexCounts(IntPtr model); + + /// Returns a pointer array of ushort index arrays per drawable. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe ushort** csmGetDrawableIndices(IntPtr model); + + /// Returns a float array of per-drawable opacities. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe float* csmGetDrawableOpacities(IntPtr model); + + /// Returns draw order indices — maps render order slot to drawable index. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int* csmGetDrawableDrawOrders(IntPtr model); + + /// Returns render order indices (newer Cubism format). + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int* csmGetRenderOrders(IntPtr model); + + /// Resets dynamic flags for all drawables, preparing for the next frame. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe void csmResetDrawableDynamicFlags(IntPtr model); + + /// Returns an int array of mask counts per drawable. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int* csmGetDrawableMaskCounts(IntPtr model); + + /// Returns a pointer array of int arrays (mask drawable indices per drawable). + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int** csmGetDrawableMasks(IntPtr model); + + /// Returns a csmVector4 array of per-drawable multiply colors. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe csmVector4* csmGetDrawableMultiplyColors(IntPtr model); + + /// Returns a csmVector4 array of per-drawable screen colors. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe csmVector4* csmGetDrawableScreenColors(IntPtr model); + + /// Returns the number of parts in the model. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int csmGetPartCount(IntPtr model); + + /// Returns a pointer array of part ID strings. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe IntPtr csmGetPartIds(IntPtr model); + + /// Returns a float array of per-part opacities. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe float* csmGetPartOpacities(IntPtr model); + + /// Checks if the given .moc3 data passes consistency validation. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe int csmHasMocConsistency(void* address, uint size); + + /// Returns the latest .moc3 format version supported by the DLL. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe uint csmGetLatestMocVersion(); + + /// Returns the .moc3 format version of the given binary data. + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe uint csmGetMocVersion(void* address, uint size); + } +} diff --git a/source_code/Cubism/Framework/ACubismMotion.cs b/source_code/Cubism/Framework/ACubismMotion.cs new file mode 100644 index 0000000..b05da30 --- /dev/null +++ b/source_code/Cubism/Framework/ACubismMotion.cs @@ -0,0 +1,182 @@ +using System; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Abstract base for all Live2D Cubism motions (animations and expressions). + /// Provides fade-in/fade-out timing, looping, weight blending, and event + /// callbacks shared across and + /// . + /// + /// Motions are managed by , which + /// calls on start, then + /// and + /// every frame. + /// + public abstract class ACubismMotion + { + /// + /// Callback invoked when the motion finishes (non-looping only). + /// The finished motion is passed as the argument. + /// + public delegate void FinishedMotionCallback(ACubismMotion motion); + + /// + /// Callback invoked when the motion begins playback. + /// Called from right after + /// the entry's times are configured. + /// + public delegate void BeganMotionCallback(ACubismMotion motion); + + protected float _fadeInSeconds = -1f; + protected float _fadeOutSeconds = -1f; + protected float _weight = 1f; + protected float _offsetSeconds; + protected bool _isLoop; + protected bool _isLoopFadeIn = true; + protected bool _previousLoopState; + + /// + /// Duration of the fade-in transition in seconds. + /// A negative value means the default (1 second) will be used + /// by and . + /// + public float FadeInSeconds { get => _fadeInSeconds; set => _fadeInSeconds = value; } + + /// + /// Duration of the fade-out transition in seconds. + /// A negative value means the default (1 second) will be used. + /// + public float FadeOutSeconds { get => _fadeOutSeconds; set => _fadeOutSeconds = value; } + + /// + /// Overall motion weight (0..1). Multiplied with the fade-in/fade-out + /// factors in to produce the final blend weight. + /// + public float Weight { get => _weight; set => _weight = value; } + + /// + /// Time offset in seconds applied to the motion's start time. + /// Allows starting playback from an arbitrary point. + /// + public float OffsetSeconds { get => _offsetSeconds; set => _offsetSeconds = value; } + + /// + /// Whether the motion loops. When enabled, + /// wraps timeOffset back to the beginning after the duration elapses. + /// + public bool IsLoop { get => _isLoop; set => _isLoop = value; } + + /// + /// When true and looping, the motion performs a fade-in at each loop + /// restart. When false, the fade-in is skipped on subsequent loops. + /// + public bool IsLoopFadeIn { get => _isLoopFadeIn; set => _isLoopFadeIn = value; } + + /// + /// Invoked when the motion finishes (non-looping only). + /// Automatically called by + /// when timeOffset exceeds duration. + /// + public FinishedMotionCallback OnFinishedMotion { get; set; } + + /// + /// Invoked when the motion begins. Fires at the start of + /// after the entry is initialized. + /// + public BeganMotionCallback OnBeganMotion { get; set; } + + /// + /// Returns the total motion duration in seconds. + /// Override in to return the motion3.json + /// duration, or -1 for looping motions. + /// + public virtual float GetDuration() => -1f; + + /// + /// Returns the loop cycle duration in seconds. + /// Override in to return the full duration + /// even when looping is active. + /// + public virtual float GetLoopDuration() => -1f; + + /// + /// Initializes a queue entry with start/end times based on current + /// , , + /// and . Fires . + /// + public void SetupMotionQueueEntry(CubismMotionQueueEntry entry, float userTimeSeconds) + { + var fadeIn = _fadeInSeconds >= 0f ? _fadeInSeconds : 1f; + entry.StartTime = userTimeSeconds - _offsetSeconds; + entry.FadeInStartTime = userTimeSeconds; + var duration = GetDuration(); + UpdateEndTime(entry, duration); + OnBeganMotion?.Invoke(this); + } + + /// + /// Computes the final frame weight = x fadeIn x fadeOut. + /// Fade curves use the sine easing function from . + /// + public float UpdateFadeWeight(CubismMotionQueueEntry entry, float userTimeSeconds) + { + var fadeIn = GetFadeIn(entry, userTimeSeconds); + var fadeOut = GetFadeOut(entry, userTimeSeconds); + return _weight * fadeIn * fadeOut; + } + + private float GetFadeIn(CubismMotionQueueEntry entry, float userTimeSeconds) + { + var fadeInTime = _fadeInSeconds >= 0f ? _fadeInSeconds : 1f; + if (fadeInTime <= 0f) return 1f; + var t = (userTimeSeconds - entry.FadeInStartTime) / fadeInTime; + return CubismMath.GetEasingSine(t); + } + + private float GetFadeOut(CubismMotionQueueEntry entry, float userTimeSeconds) + { + var fadeOutTime = _fadeOutSeconds >= 0f ? _fadeOutSeconds : 1f; + if (fadeOutTime <= 0f) return 1f; + var t = (entry.EndTime - userTimeSeconds) / fadeOutTime; + return CubismMath.GetEasingSine(t); + } + + /// + /// Sets the queue entry's EndTime based on loop state and duration. + /// Looping motions get EndTime = -1 (infinite). + /// + protected void UpdateEndTime(CubismMotionQueueEntry entry, float duration) + { + entry.EndTime = _isLoop ? -1f : entry.StartTime + duration; + } + + /// + /// Recalculates EndTime when changes mid-playback. + /// Called by every frame + /// to detect loop state transitions. + /// + protected void AdjustEndTime(CubismMotionQueueEntry entry, float duration) + { + if (_isLoop != _previousLoopState) + { + _previousLoopState = _isLoop; + UpdateEndTime(entry, duration); + } + } + + /// + /// Applies motion parameter values to the model for the current frame. + /// Called every frame by . + /// + /// The native Cubism model to modify. + /// Elapsed user time in seconds. + /// Blend weight already multiplied by fade factors. + /// The queue entry containing start/end timing. + public abstract void DoUpdateParameters( + CubismNativeModel model, + float userTimeSeconds, + float weight, + CubismMotionQueueEntry entry); + } +} diff --git a/source_code/Cubism/Framework/CubismBreath.cs b/source_code/Cubism/Framework/CubismBreath.cs new file mode 100644 index 0000000..bdc2b79 --- /dev/null +++ b/source_code/Cubism/Framework/CubismBreath.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Defines a sinusoidal breathing parameter. Each breath parameter + /// oscillates a model parameter value around an offset with a given + /// peak amplitude, cycle period, and weight. + /// + public struct BreathParameterData + { + /// Model parameter ID to apply the breath to. + public string ParameterId; + + /// Base offset added to the sinusoidal value. + public float Offset; + + /// Amplitude of the sine wave. + public float Peak; + + /// Full cycle duration in seconds. + public float Cycle; + + /// Multiplier applied to the final breath value. + public float Weight; + } + + /// + /// Applies sinusoidal breathing animation to model parameters. + /// Operates additively on top of existing parameter values — + /// the breath oscillation is multiplied by + /// and added to the current parameter value every frame. + /// + /// Configured with a list of entries, + /// each targeting a specific parameter ID. Called from + /// alongside other effect layers + /// (eye blink, pose, physics). + /// + public class CubismBreath + { + private readonly List _parameters = new(); + private float _currentTime; + + /// + /// Replaces the current breath parameter set. + /// Previously configured parameters are cleared. + /// + public void SetParameters(List parameters) + { + _parameters.Clear(); + _parameters.AddRange(parameters); + } + + /// + /// Applies the breath oscillation to matching model parameters. + /// Accumulates internal time and evaluates a sine wave per parameter: + /// offset + peak * sin(2 * pi * time / cycle), then adds + /// the weighted result to the native parameter value. + /// + public unsafe void UpdateParameters(CubismNativeModel model, float deltaTimeSeconds) + { + _currentTime += deltaTimeSeconds; + + var idsPtr = model.GetParameterIds(); + var values = model.GetParameterValues(); + + for (int i = 0; i < model.ParameterCount; i++) + { + var id = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i); + foreach (var bp in _parameters) + { + if (bp.ParameterId != id) continue; + var value = bp.Offset + bp.Peak * MathF.Sin(2f * MathF.PI * _currentTime / bp.Cycle); + values[i] += bp.Weight * value; + break; + } + } + } + } +} diff --git a/source_code/Cubism/Framework/CubismExpressionMotion.cs b/source_code/Cubism/Framework/CubismExpressionMotion.cs new file mode 100644 index 0000000..16b4e05 --- /dev/null +++ b/source_code/Cubism/Framework/CubismExpressionMotion.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Blend mode for expression parameters. + /// Determines how the expression value is combined with the existing + /// parameter value during animation. + /// + public enum ExpressionBlendType { Additive = 0, Multiply = 1, Overwrite = 2 } + + /// + /// A single parameter entry in an expression motion. + /// Targets a model parameter by ID with a specific blend type and value. + /// + public struct ExpressionParameter + { + /// Target model parameter ID. + public string ParameterId; + + /// How to combine this value with existing parameter data. + public ExpressionBlendType BlendType; + + /// The target value (interpretation depends on ). + public float Value; + } + + /// + /// Expression motion, parsed from a Cubism .exp3.json file. + /// Inherits fade and weight management from . + /// + /// Expressions are single-frame poses that transition between values using + /// one of three blend modes per parameter: + /// + /// — adds value * weight + /// — multiplies by 1 + value * weight + /// — lerps from current to target using weight + /// + /// + /// Loaded and started by . + /// + public class CubismExpressionMotion : ACubismMotion + { + private readonly List _parameters = new(); + + /// + /// Parses an .exp3.json byte buffer into an expression motion. + /// Reads fade-in/out times and the list of target parameters with + /// their blend types and values. + /// + public CubismExpressionMotion(byte[] jsonBytes) + { + var json = JsonDocument.Parse(jsonBytes); + var root = json.RootElement; + + if (root.TryGetProperty("FadeInTime", out var fi)) + _fadeInSeconds = fi.GetSingle(); + if (root.TryGetProperty("FadeOutTime", out var fo)) + _fadeOutSeconds = fo.GetSingle(); + + if (root.TryGetProperty("Parameters", out var paramsArr)) + { + foreach (var p in paramsArr.EnumerateArray()) + { + var blend = p.TryGetProperty("Blend", out var b) + ? ParseBlendType(b.GetString()) + : ExpressionBlendType.Additive; + + _parameters.Add(new ExpressionParameter + { + ParameterId = p.GetProperty("Id").GetString(), + Value = p.GetProperty("Value").GetSingle(), + BlendType = blend + }); + } + } + } + + private static ExpressionBlendType ParseBlendType(string value) => value switch + { + "Multiply" => ExpressionBlendType.Multiply, + "Overwrite" => ExpressionBlendType.Overwrite, + _ => ExpressionBlendType.Additive + }; + + /// + /// Applies all expression parameters to the model using their + /// respective blend modes. Called every frame by the queue manager. + /// + public override unsafe void DoUpdateParameters( + CubismNativeModel model, float userTimeSeconds, float weight, CubismMotionQueueEntry entry) + { + var idsPtr = model.GetParameterIds(); + var values = model.GetParameterValues(); + var minValues = model.GetParameterMinimumValues(); + var maxValues = model.GetParameterMaximumValues(); + var defaultValues = model.GetParameterDefaultValues(); + + for (int pi = 0; pi < model.ParameterCount; pi++) + { + var pid = CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi); + foreach (var ep in _parameters) + { + if (ep.ParameterId != pid) continue; + + switch (ep.BlendType) + { + case ExpressionBlendType.Additive: + values[pi] = values[pi] + ep.Value * weight; + break; + case ExpressionBlendType.Multiply: + values[pi] = values[pi] * (1f + ep.Value * weight); + break; + case ExpressionBlendType.Overwrite: + values[pi] = values[pi] * (1f - weight) + ep.Value * weight; + break; + } + break; + } + } + } + } +} diff --git a/source_code/Cubism/Framework/CubismEyeBlink.cs b/source_code/Cubism/Framework/CubismEyeBlink.cs new file mode 100644 index 0000000..e1bd07e --- /dev/null +++ b/source_code/Cubism/Framework/CubismEyeBlink.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Drives a state-machine based eye blink animation on target model parameters. + /// Cycles through five states: First -> Interval -> Closing -> Closed -> Opening, + /// modulating the specified parameter values from 1 (open) to 0 (closed) and back. + /// + /// Blink timing is randomized per cycle: each interval between blinks is a random + /// value in (0, 2 * BlinkIntervalSeconds). The actual blink consists of + /// configurable closing, closed, and opening phase durations. + /// + /// Used by when + /// is true and the model3.json defines EyeBlink parameter group. + /// + public class CubismEyeBlink + { + private enum EyeState { First, Interval, Closing, Closed, Opening } + + private EyeState _state = EyeState.First; + private float _nextBlinkTime; + private float _stateStartTime; + private float _userTimeSeconds; + + private readonly List _parameterIds = new(); + + /// + /// Average interval between successive blinks, in seconds. + /// Actual interval is randomized in range (0, 2 * this value). + /// Default is 4 seconds. + /// + public float BlinkIntervalSeconds { get; set; } = 4f; + + /// + /// Duration of the eyelid closing transition, in seconds. Default 0.1. + /// + public float ClosingSeconds { get; set; } = 0.1f; + + /// + /// Duration the eyes stay fully closed, in seconds. Default 0.05. + /// + public float ClosedSeconds { get; set; } = 0.05f; + + /// + /// Duration of the eyelid opening transition, in seconds. Default 0.15. + /// + public float OpeningSeconds { get; set; } = 0.15f; + + /// + /// Sets the parameter IDs that represent eye open/close. + /// Typically sourced from the model3.json EyeBlink group. + /// Clears any previously set IDs. + /// + public void SetParameterIds(IEnumerable ids) + { + _parameterIds.Clear(); + _parameterIds.AddRange(ids); + } + + /// + /// Advances the blink state machine and writes eye openness to model parameters. + /// 1.0 = fully open, 0.0 = fully closed. Linear interpolation between phases. + /// Call every frame from . + /// + public void UpdateParameters(CubismNativeModel model, float deltaTimeSeconds) + { + _userTimeSeconds += deltaTimeSeconds; + + switch (_state) + { + case EyeState.First: + _state = EyeState.Interval; + _nextBlinkTime = _userTimeSeconds + Random.Shared.NextSingle() * (2f * BlinkIntervalSeconds - 1f); + break; + + case EyeState.Interval: + SetParameterValues(model, 1f); + if (_userTimeSeconds >= _nextBlinkTime) + { + _state = EyeState.Closing; + _stateStartTime = _userTimeSeconds; + } + break; + + case EyeState.Closing: + { + var elapsed = _userTimeSeconds - _stateStartTime; + var value = 1f - Math.Clamp(elapsed / ClosingSeconds, 0f, 1f); + SetParameterValues(model, value); + if (elapsed >= ClosingSeconds) + { + _state = EyeState.Closed; + _stateStartTime = _userTimeSeconds; + } + break; + } + + case EyeState.Closed: + SetParameterValues(model, 0f); + if (_userTimeSeconds - _stateStartTime >= ClosedSeconds) + { + _state = EyeState.Opening; + _stateStartTime = _userTimeSeconds; + } + break; + + case EyeState.Opening: + { + var elapsed = _userTimeSeconds - _stateStartTime; + var value = Math.Clamp(elapsed / OpeningSeconds, 0f, 1f); + SetParameterValues(model, value); + if (elapsed >= OpeningSeconds) + { + _state = EyeState.Interval; + _nextBlinkTime = _userTimeSeconds + Random.Shared.NextSingle() * (2f * BlinkIntervalSeconds - 1f); + } + break; + } + } + } + + private unsafe void SetParameterValues(CubismNativeModel model, float value) + { + var idsPtr = model.GetParameterIds(); + var values = model.GetParameterValues(); + for (int i = 0; i < model.ParameterCount; i++) + { + var id = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i); + if (_parameterIds.Contains(id)) + values[i] = value; + } + } + } +} diff --git a/source_code/Cubism/Framework/CubismMath.cs b/source_code/Cubism/Framework/CubismMath.cs new file mode 100644 index 0000000..f65d259 --- /dev/null +++ b/source_code/Cubism/Framework/CubismMath.cs @@ -0,0 +1,202 @@ +using System; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Math utilities shared across the Cubism animation pipeline. + /// Includes easing functions (sine), angle utilities, linear + /// interpolation helpers, and bezier curve evaluators with both + /// analytic (Cardano) and binary search approaches. + /// + /// The bezier methods handle Cubism's segmented animation curves: + /// calculates a value at a given time, + /// solves for the exact + /// bezier parameter t, and + /// is a fallback when beziers are restricted (non-standard). + /// + public static class CubismMath + { + /// + /// Sine-based easing function mapping [0,1] -> [0,1] with smooth + /// acceleration and deceleration. Used for fade-in/fade-out curves + /// in motion transitions. + /// 0.5 - 0.5 * cos(pi * t), clamped to [0,1]. + /// + public static float GetEasingSine(float t) + { + if (t < 0f) return 0f; + if (t > 1f) return 1f; + return 0.5f - 0.5f * MathF.Cos(MathF.PI * t); + } + + /// Converts degrees to radians. + public static float DegreesToRadian(float degrees) + { + return degrees * (MathF.PI / 180f); + } + + /// Converts radians to degrees. + public static float RadianToDegrees(float radian) + { + return radian * (180f / MathF.PI); + } + + /// + /// Computes the signed angle in radians from vector (fromX,fromY) to vector (toX,toY). + /// Uses Atan2(det, dot) for the angle between two vectors. + /// + public static float DirectionToRadian(float fromX, float fromY, float toX, float toY) + { + var dot = fromX * toX + fromY * toY; + var det = fromX * toY - fromY * toX; + return MathF.Atan2(det, dot); + } + + /// + /// Converts a radian angle to a unit direction vector (sin, cos). + /// + public static void RadianToDirection(float radian, out float x, out float y) + { + x = MathF.Sin(radian); + y = MathF.Cos(radian); + } + + /// Standard linear interpolation between two float values. + public static float LinearEvaluation(float startValue, float endValue, float time) + { + return startValue + (endValue - startValue) * time; + } + + /// + /// Linear interpolation between two values, + /// using their stored Time properties to compute the interpolation factor. + /// + public static float LinearEvaluation(ref CubismMotionPoint p0, ref CubismMotionPoint p1, float time) + { + var t = (time - p0.Time) / (p1.Time - p0.Time); + return p0.Value + (p1.Value - p0.Value) * t; + } + + /// + /// De Casteljau cubic bezier evaluation. + /// Given 4 control points (cp0..cp3), each with a time and value, + /// returns the bezier value at the specified time. + /// + public static float BezierEvaluate( + float cp0Time, float cp0Value, + float cp1Time, float cp1Value, + float cp2Time, float cp2Value, + float cp3Time, float cp3Value, + float time) + { + var t = (time - cp0Time) / (cp3Time - cp0Time); + var t1 = 1f - t; + var t2 = t * t; + var t12 = t1 * t1; + + var value = + t12 * t1 * cp0Value + + 3f * t12 * t * cp1Value + + 3f * t1 * t2 * cp2Value + + t2 * t * cp3Value; + + return value; + } + + /// + /// Cardano cubic formula to solve for the exact bezier parameter t + /// given a target time. Used when beziers are not restricted — + /// the analytic solution is faster than binary search. + /// + /// Returns the t value that, when applied to the bezier equation, + /// produces the targetTime (or very close to it). + /// + public static float CardanoAlgorithmForBezier( + float cp0Time, float cp1Time, float cp2Time, float cp3Time, + float targetTime) + { + var dx = cp0Time - targetTime; + var cx = cp1Time - cp0Time; + var bx = cp2Time - cp1Time; + var ax = cp3Time - cp2Time; + var a2 = ax - cx; + var ab = 2f * (ax - 2f * bx + cx); + var a0 = 3f * (bx - cx); + var ad = -cx; + var a = a2 / dx; + var b = ab / dx; + var c = a0 / dx; + var d = ad / dx; + var r = (-2f * b * b * b + 9f * a * b * c - 27f * a * a * d) / (54f * a * a * a); + var q = (b * b - 3f * a * c) / (9f * a * a); + var q3 = q * q * q; + var floorTerm = r * r - q3; + if (MathF.Abs(floorTerm) < 1e-7f) floorTerm = 0f; + if (floorTerm < 0f) + { + var theta = MathF.Acos(r / MathF.Sqrt(q3)); + var sqrtQ = MathF.Sqrt(q); + return -2f * sqrtQ * MathF.Cos(theta / 3f) - b / (3f * a); + } + var sqrtTerm = MathF.Sqrt(floorTerm); + var aTerm = MathF.Cbrt(MathF.Abs(r + sqrtTerm)); + var bTerm = r > 0f + ? MathF.Cbrt(MathF.Abs(r - sqrtTerm)) + : -MathF.Cbrt(MathF.Abs(r - sqrtTerm)); + return aTerm - bTerm - b / (3f * a); + } + + /// + /// Binary search bezier evaluation — fallback for when + /// is true. + /// Iteratively narrows t in [0,1] until the bezier time at t + /// is within epsilon of the target time, then evaluates the value. + /// Capped at 20 iterations with 1e-5 tolerance. + /// + public static float BezierEvaluateBinarySearch( + float cp0Time, float cp1Time, float cp2Time, float cp3Time, + float cp0Value, float cp1Value, float cp2Value, float cp3Value, + float targetTime) + { + const int maxIterations = 20; + const float epsilon = 1e-5f; + var tMin = 0f; + var tMax = 1f; + + for (var i = 0; i < maxIterations; i++) + { + var t = (tMin + tMax) * 0.5f; + var t1 = 1f - t; + var t2 = t * t; + var t12 = t1 * t1; + var timeAtT = + t12 * t1 * cp0Time + + 3f * t12 * t * cp1Time + + 3f * t1 * t2 * cp2Time + + t2 * t * cp3Time; + + if (MathF.Abs(timeAtT - targetTime) < epsilon) + return + t12 * t1 * cp0Value + + 3f * t12 * t * cp1Value + + 3f * t1 * t2 * cp2Value + + t2 * t * cp3Value; + + if (timeAtT < targetTime) + tMin = t; + else + tMax = t; + } + + var tf = (tMin + tMax) * 0.5f; + var tf1 = 1f - tf; + var tf2 = tf * tf; + var tf12 = tf1 * tf1; + return + tf12 * tf1 * cp0Value + + 3f * tf12 * tf * cp1Value + + 3f * tf1 * tf2 * cp2Value + + tf2 * tf * cp3Value; + } + } +} diff --git a/source_code/Cubism/Framework/CubismModelMatrix.cs b/source_code/Cubism/Framework/CubismModelMatrix.cs new file mode 100644 index 0000000..dd23988 --- /dev/null +++ b/source_code/Cubism/Framework/CubismModelMatrix.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using Godot; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Manages the 2D transformation (scale, translation, alignment) for a + /// Cubism model rendered inside a Godot . + /// + /// Works with two coordinate systems: + /// + /// Cubism canvas coordinates (the model's native coordinate space) + /// Godot node local coordinates (where the model is placed) + /// + /// + /// The scale is computed relative to the canvas dimensions stored at construction. + /// and set a uniform scale based + /// on the ratio of target size to canvas size. reads + /// positioning keys from a Cubism .model3.json "Layout" section. + /// writes the final transform to a Godot Node2D. + /// + public class CubismModelMatrix + { + private float _width; + private float _height; + private float _scaleX = 1f; + private float _scaleY = 1f; + private float _translateX; + private float _translateY; + + /// Creates an identity matrix with no known canvas size. + public CubismModelMatrix() { } + + /// + /// Creates a matrix referenced to the given canvas pixel dimensions. + /// Subsequent calls to and + /// will compute scale as target / canvas. + /// + public CubismModelMatrix(float width, float height) + { + _width = width; + _height = height; + } + + /// Current uniform scale on the X axis. + public float ScaleX => _scaleX; + + /// Current uniform scale on the Y axis. + public float ScaleY => _scaleY; + + /// Current X translation offset in Godot local coordinates. + public float TranslateX => _translateX; + + /// Current Y translation offset in Godot local coordinates. + public float TranslateY => _translateY; + + /// + /// Sets uniform scale to match the given width. Scale = targetWidth / canvasWidth. + /// Does nothing if canvas width is zero or unknown. + /// + public void SetWidth(float w) + { + if (_width > 0f) + { + var scale = w / _width; + _scaleX = scale; + _scaleY = scale; + } + } + + /// + /// Sets uniform scale to match the given height. Scale = targetHeight / canvasHeight. + /// Does nothing if canvas height is zero or unknown. + /// + public void SetHeight(float h) + { + if (_height > 0f) + { + var scale = h / _height; + _scaleX = scale; + _scaleY = scale; + } + } + + /// Sets absolute translation position. + public void SetPosition(float x, float y) + { + _translateX = x; + _translateY = y; + } + + /// Sets absolute X translation. + public void SetX(float x) => _translateX = x; + + /// Sets absolute Y translation. + public void SetY(float y) => _translateY = y; + + /// + /// Centers the model horizontally and vertically at the given point. + /// Equivalent to calling then . + /// + public void SetCenterPosition(float x, float y) + { + CenterX(x); + CenterY(y); + } + + /// + /// Centers horizontally: translateX = x - (width * scaleX) / 2. + /// + public void CenterX(float x) => _translateX = x - (_width * _scaleX) / 2f; + + /// + /// Centers vertically: translateY = y - (height * scaleY) / 2. + /// + public void CenterY(float y) => _translateY = y - (_height * _scaleY) / 2f; + + /// Aligns the top edge of the model to y. + public void Top(float y) => _translateY = y; + + /// Aligns the bottom edge of the model to y. + public void Bottom(float y) => _translateY = y - _height * _scaleY; + + /// Aligns the left edge of the model to x. + public void Left(float x) => _translateX = x; + + /// Aligns the right edge of the model to x. + public void Right(float x) => _translateX = x - _width * _scaleX; + + /// + /// Configures scale and position from a layout dictionary. + /// Supported keys: "width", "height", "x", "y", "center_x", "center_y", + /// "top", "bottom", "left", "right". + /// Width/height are applied first (since they affect scale), + /// then position keys are applied. + /// + public void SetupFromLayout(Dictionary layout) + { + foreach (var kvp in layout) + { + switch (kvp.Key) + { + case "width": SetWidth(kvp.Value); break; + case "height": SetHeight(kvp.Value); break; + } + } + + foreach (var kvp in layout) + { + switch (kvp.Key) + { + case "x": SetX(kvp.Value); break; + case "y": SetY(kvp.Value); break; + case "center_x": CenterX(kvp.Value); break; + case "center_y": CenterY(kvp.Value); break; + case "top": Top(kvp.Value); break; + case "bottom": Bottom(kvp.Value); break; + case "left": Left(kvp.Value); break; + case "right": Right(kvp.Value); break; + } + } + } + + /// + /// Writes the current scale and translation to a Godot . + /// Sets both Scale and Position properties. + /// + public void ApplyToNode(Node2D node) + { + node.Scale = new Vector2(_scaleX, _scaleY); + node.Position = new Vector2(_translateX, _translateY); + } + } +} diff --git a/source_code/Cubism/Framework/CubismModelNode.cs b/source_code/Cubism/Framework/CubismModelNode.cs new file mode 100644 index 0000000..169dae0 --- /dev/null +++ b/source_code/Cubism/Framework/CubismModelNode.cs @@ -0,0 +1,520 @@ +using Cthangover.Core.Mods; +using Cthangover.Core.UI.Event; +using Cthangover.Core.Utils; +using Godot; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Defines how the Live2D model scales to fit its parent container. + /// + public enum FitMode { None, FitWidth, FitHeight, FitAuto } + + /// + /// The main Godot node that loads, updates, and renders a Live2D Cubism model. + /// Represents a single character/avatar in the scene tree. + /// + /// Orchestrates the full pipeline: + /// + /// Loads .model3.json and .moc3 via + /// Sets up and arrays + /// Creates optional effect layers: , , + /// , + /// Builds 2D meshes through + /// Every frame (): target point -> parameter injection -> + /// motion/expression evaluation -> effect layers -> model update -> renderer update + /// + /// + /// The update order matters: motion values are applied first, then expression values + /// blend on top, and finally effect layers (eye blink, breath, pose, physics) modify + /// the result. The target point drives face-tracking parameters (ParamAngleX/Y, ParamEyeBallX/Y). + /// + public partial class CubismModelNode : Node2D, IOnUpdateEvent + { + public int Priority => 0; + private CubismNativeModel _nativeModel; + private CubismRenderer2D _renderer; + private CubismModelSettingJson _modelSetting; + + private CubismEyeBlink _eyeBlink; + private CubismBreath _breath; + private CubismPose _pose; + private CubismPhysics _physics; + private CubismTargetPoint _targetPoint = new(); + + private Vector2 _contentSize; + private Vector2 _contentCenter; + private bool _contentBoundsReady; + + private CubismMotionQueueManager _motionManager = new(); + private CubismMotionQueueManager _expressionManager = new(); + + private float _userTimeSeconds; + private string _modelDir; + private IModFileProvider _fileProvider; + private bool _layoutLogged; + private readonly System.Collections.Generic.HashSet _manualParameters = new(); + + /// + /// All model parameters with their IDs, bounds, and current values. + /// Directly references the native parameter arrays — modifying + /// and calling + /// writes back to CubismCore. + /// + public CubismParameter[] Parameters { get; private set; } = System.Array.Empty(); + + /// + /// All model part opacities, indexed by part index. + /// Useful for showing/hiding parts (clothing layers, accessories, etc.). + /// + public CubismPartOpacity[] PartOpacities { get; private set; } = System.Array.Empty(); + + /// Canvas pixel width from the model's .moc3 data. Available after loading. + public float CanvasWidth => _nativeModel?.CanvasWidth ?? 0f; + + /// Canvas pixel height from the model's .moc3 data. Available after loading. + public float CanvasHeight => _nativeModel?.CanvasHeight ?? 0f; + + /// + /// Horizontal anchor point for layout positioning, 0..1. + /// 0 = left edge, 0.5 = center, 1 = right edge. Default 0.5. + /// Serialized as a Godot export property. + /// + [Export] public float AnchorX { get; set; } = 0.5f; + + /// + /// Vertical anchor point for layout positioning, 0..1. + /// 0 = top edge, 0.5 = center, 1 = bottom edge. Default 0.5. + /// + [Export] public float AnchorY { get; set; } = 0.5f; + + /// + /// How the model scales to fit its parent container. Default . + /// Used by on the first frame with a valid parent size. + /// + [Export] public FitMode ScaleFit { get; set; } = FitMode.FitAuto; + + /// + /// Enables automatic eye blink animation. Requires EyeBlink parameter groups + /// in the model3.json. Default true. + /// + [Export] public bool EnableEyeBlink { get; set; } = true; + + /// + /// Enables breathing animation. Default false. + /// Note: breath parameters are not currently loaded from model settings — + /// enable this only if parameters are set externally via . + /// + [Export] public bool EnableBreath { get; set; } + + /// + /// Enables physics simulation (hair, cloth, etc.) from the .physics3.json file. + /// Default true. + /// + [Export] public bool EnablePhysics { get; set; } = true; + + /// + /// Enables pose-based part opacity management from the .pose3.json file. + /// Default true. + /// + [Export] public bool EnablePose { get; set; } = true; + + private bool _loaded; + + /// + /// Loads a Cubism model from within a registered mod. + /// Reads the .model3.json file, resolves the .moc3 and textures, then + /// calls to build parameters, parts, effects, and meshes. + /// When is null, all mods are searched in priority + /// order for the first match. + /// + /// Path to .model3.json relative to the mod root. + /// Optional registered mod ID containing the model assets. + public void LoadModelFromMod(string relativePath, string modId = null) + { + var mods = ModManager.Instance?.Mods; + if (mods == null) + { + GameLogger.Log("LIVE2D", $"LoadModelFromMod: no mods loaded", LogLevel.Error); + return; + } + + IModInfo modInfo = null; + + if (modId != null) + { + mods.TryGetValue(modId, out modInfo); + } + else + { + var orderedIds = ModRegistry.Instance.GetOrderedModIds(); + foreach (var id in orderedIds) + { + if (mods.TryGetValue(id, out var candidate) && candidate.FileProvider?.FileExists(relativePath) == true) + { + modInfo = candidate; + break; + } + } + } + + if (modInfo == null) + { + GameLogger.Log("LIVE2D", $"LoadModelFromMod: file '{relativePath}' not found in any mod", LogLevel.Error); + return; + } + + _fileProvider = modInfo.FileProvider; + var jsonBytes = _fileProvider.ReadFileBinary(relativePath); + if (jsonBytes == null) + { + GameLogger.Log("LIVE2D", $"LoadModelFromMod: file not found '{relativePath}' in mod '{modId ?? "?"}'", LogLevel.Error); + return; + } + + _modelDir = relativePath.Contains('/') + ? relativePath.Substring(0, relativePath.LastIndexOf('/')) + : ""; + + _nativeModel = new CubismNativeModel(); + _nativeModel.LoadFromProvider(jsonBytes, _modelDir, _fileProvider); + FinishSetup(jsonBytes); + } + + private void FinishSetup(byte[] model3JsonBytes) + { + _modelSetting = new CubismModelSettingJson(model3JsonBytes); + SetupParameters(); + SetupParts(); + SetupEffects(); + + _renderer = new CubismRenderer2D(this); + _nativeModel.UpdateModel(); + _renderer.BuildModel(_nativeModel); + + var bounds = _renderer.GetContentBounds(); + _contentSize = new Vector2(bounds.Size.X, bounds.Size.Y); + _contentCenter = new Vector2(bounds.GetCenter().X, bounds.GetCenter().Y); + _contentBoundsReady = true; + GameLogger.Log("LIVE2D", $"contentBounds: center={_contentCenter} size={_contentSize}"); + + _loaded = true; + + var ec = GetNodeOrNull("/root/EventController"); + if (ec != null) + ec.AddUpdateEventListener(this); + + GameLogger.Log("LIVE2D", $"Model loaded: {_nativeModel.ParameterCount} params, {_nativeModel.DrawableCount} drawables, canvas={_nativeModel.CanvasWidth}x{_nativeModel.CanvasHeight}"); + } + + private ulong _lastTickMsec; + + /// + /// Per-frame update called by the SceneEventController. + /// Execution order: + /// + /// Apply deferred layout (first valid frame only) + /// Update for face tracking + /// Inject face-tracking parameters (ParamAngleX/Y, ParamEyeBallX/Y) + /// into the native model + /// Evaluate motion queue (character animations) + /// Evaluate expression queue (facial expressions) + /// Run effect layers in order: breath, eye blink, pose, physics + /// Call csmUpdateModel on the native model + /// Update the renderer (visibility, opacity, vertex positions) + /// + /// + public void OnUpdate() + { + if (!_loaded) return; + + var now = Time.GetTicksMsec(); + var dt = _lastTickMsec == 0 ? 0.016f : (now - _lastTickMsec) / 1000f; + _lastTickMsec = now; + + DeferredApplyLayout(); + + _userTimeSeconds += dt; + + _targetPoint.Update(dt); + var tx = _targetPoint.X; + var ty = _targetPoint.Y; + + unsafe + { + var values = _nativeModel.GetParameterValues(); + var idsPtr = _nativeModel.GetParameterIds(); + + for (int i = 0; i < _nativeModel.ParameterCount; i++) + { + var id = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i); + if (_manualParameters.Contains(id)) continue; + switch (id) + { + case "ParamAngleX": values[i] = tx * 30f; break; + case "ParamAngleY": values[i] = ty * 30f; break; + case "ParamEyeBallX": values[i] = tx; break; + case "ParamEyeBallY": values[i] = ty; break; + } + } + } + + _motionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds); + _expressionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds); + + _breath?.UpdateParameters(_nativeModel, dt); + _eyeBlink?.UpdateParameters(_nativeModel, dt); + _pose?.UpdateParameters(_nativeModel, dt); + _physics?.Evaluate(_nativeModel, dt); + + _nativeModel.UpdateModel(); + _renderer.UpdateModel(); + } + + private void DeferredApplyLayout() + { + if (_layoutLogged) return; + if (!_contentBoundsReady) return; + + var parentSize = GetParentSize(); + if (parentSize.X <= 0f || parentSize.Y <= 0f) return; + + var cw = _contentSize.X; + var ch = _contentSize.Y; + if (cw <= 0f || ch <= 0f) return; + + float scale = 1f; + switch (ScaleFit) + { + case FitMode.FitWidth: scale = parentSize.X / cw; break; + case FitMode.FitHeight: scale = parentSize.Y / ch; break; + case FitMode.FitAuto: scale = Mathf.Min(parentSize.X / cw, parentSize.Y / ch); break; + } + + Scale = new Vector2(scale, scale); + Position = new Vector2( + parentSize.X * AnchorX - _contentCenter.X * scale, + parentSize.Y * AnchorY - _contentCenter.Y * scale); + + _layoutLogged = true; + GameLogger.Log("LIVE2D", $"ApplyLayout: parentSize={parentSize} contentSize={cw}x{ch} contentCenter={_contentCenter} scale={scale} pos={Position}"); + } + + private Vector2 GetParentSize() + { + var parent = GetParent(); + if (parent is Control c && c.Size.X > 0f && c.Size.Y > 0f) + return c.Size; + return GetViewportRect().Size; + } + + private unsafe void SetupParameters() + { + var count = _nativeModel.ParameterCount; + Parameters = new CubismParameter[count]; + var idsPtr = _nativeModel.GetParameterIds(); + var mins = _nativeModel.GetParameterMinimumValues(); + var maxs = _nativeModel.GetParameterMaximumValues(); + var defaults = _nativeModel.GetParameterDefaultValues(); + + for (int i = 0; i < count; i++) + { + Parameters[i] = new CubismParameter(_nativeModel) + { + Id = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i), + Index = i, + MinimumValue = mins[i], + MaximumValue = maxs[i], + DefaultValue = defaults[i], + Value = defaults[i] + }; + } + } + + private unsafe void SetupParts() + { + var count = _nativeModel.GetPartCount(); + PartOpacities = new CubismPartOpacity[count]; + var idsPtr = _nativeModel.GetPartIds(); + var opacities = _nativeModel.GetPartOpacities(); + + for (int i = 0; i < count; i++) + { + PartOpacities[i] = new CubismPartOpacity(_nativeModel) + { + Id = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i), + Index = i, + Value = opacities[i] + }; + } + } + + private void SetupEffects() + { + if (EnableEyeBlink && _modelSetting.GetEyeBlinkParameterCount() > 0) + { + _eyeBlink = new CubismEyeBlink(); + var ids = new System.Collections.Generic.List(); + for (int i = 0; i < _modelSetting.GetEyeBlinkParameterCount(); i++) + ids.Add(_modelSetting.GetEyeBlinkParameterId(i)); + _eyeBlink.SetParameterIds(ids); + } + + var physicsPath = _modelSetting.GetPhysicsFileName(); + if (EnablePhysics && !string.IsNullOrEmpty(physicsPath)) + { + var physBytes = ReadModFile(physicsPath); + if (physBytes != null) + _physics = new CubismPhysics(physBytes); + else + GameLogger.Log("LIVE2D", $"SetupEffects: failed to read physics '{physicsPath}'", LogLevel.Warning); + } + + var posePath = _modelSetting.GetPoseFileName(); + if (EnablePose && !string.IsNullOrEmpty(posePath)) + { + var poseBytes = ReadModFile(posePath); + if (poseBytes != null) + _pose = new CubismPose(poseBytes); + else + GameLogger.Log("LIVE2D", $"SetupEffects: failed to read pose '{posePath}'", LogLevel.Warning); + } + } + + private byte[] ReadModFile(string relativePath) + { + var normalized = relativePath.Replace('\\', '/'); + var combined = string.IsNullOrEmpty(_modelDir) ? normalized : $"{_modelDir}/{normalized}"; + var result = _fileProvider.ReadFileBinary(combined); + if (result == null) + GameLogger.Log("LIVE2D", $"ReadModFile: provider returned null for '{combined}'", LogLevel.Warning); + return result; + } + + + /// + /// Sets the face-tracking target. Drives eye/head movement parameters + /// (ParamAngleX/Y, ParamEyeBallX/Y) through a smooth damped approach + /// in . + /// + public void SetTargetPoint(float x, float y) => _targetPoint.Set(x, y); + + /// + /// Finds a parameter by its Cubism ID string (e.g. "ParamAngleX"). + /// Returns null if not found. + /// + public CubismParameter FindParameter(string id) + { + foreach (var p in Parameters) + if (p.Id == id) return p; + return null; + } + + /// + /// Finds a part by its Cubism ID string. Returns null if not found. + /// + public CubismPartOpacity FindPart(string id) + { + foreach (var p in PartOpacities) + if (p.Id == id) return p; + return null; + } + + /// + /// Sets a parameter value directly in the native model, bypassing the per-frame + /// face-tracking injection in . Once a parameter is set + /// via this method, stops overwriting it — the caller + /// takes full control of that parameter's value. + /// Safe to call before the model is loaded — silently ignored. + /// + /// Cubism parameter ID (e.g. "ParamAngleX"). + /// Raw parameter value. Caller should clamp to the parameter's bounds. + public void SetLiveParameter(string id, float value) + { + if (!_loaded) + { + GameLogger.Log("LIVE2D", $"SetLiveParameter: model not loaded, ignoring id='{id}'", LogLevel.Warning); + return; + } + + unsafe + { + var values = _nativeModel.GetParameterValues(); + var idsPtr = _nativeModel.GetParameterIds(); + + for (int i = 0; i < _nativeModel.ParameterCount; i++) + { + var paramId = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i); + if (paramId != id) continue; + + values[i] = value; + _manualParameters.Add(id); + + if (i < Parameters.Length) + Parameters[i].Value = value; + + return; + } + } + + GameLogger.Log("LIVE2D", $"SetLiveParameter: parameter '{id}' not found", LogLevel.Warning); + } + + /// + /// Starts a motion animation by group name and index. + /// Loads the motion3.json from the model directory, applies the + /// model-specified fade-in/fade-out times, and enqueues it. + /// Safe to call before the model is loaded — the call is silently ignored. + /// + /// Motion group name from model3.json (e.g. "Idle", "TapBody"). + /// Zero-based index within the group. + public void StartMotion(string group, int no) + { + if (!_loaded) return; + if (no < 0 || no >= _modelSetting.GetMotionCount(group)) return; + + var motionFileName = _modelSetting.GetMotionFileName(group, no); + var motionBytes = ReadModFile(motionFileName); + if (motionBytes == null) return; + + var motion = new CubismMotion(motionBytes); + + var fadeIn = _modelSetting.GetMotionFadeInTimeValue(group, no); + var fadeOut = _modelSetting.GetMotionFadeOutTimeValue(group, no); + if (fadeIn >= 0f) motion.FadeInSeconds = fadeIn; + if (fadeOut >= 0f) motion.FadeOutSeconds = fadeOut; + + _motionManager.StartMotion(motion, true, _userTimeSeconds); + } + + /// + /// Starts an expression by its name (as defined in the model3.json + /// Expressions array). Looks up the expression file, loads the + /// .exp3.json, and enqueues it in the expression manager. + /// Safe to call before model loaded — silently ignored. + /// + public void StartExpression(string expressionId) + { + if (!_loaded) return; + + for (int i = 0; i < _modelSetting.GetExpressionCount(); i++) + { + if (_modelSetting.GetExpressionName(i) == expressionId) + { + var expBytes = ReadModFile(_modelSetting.GetExpressionFileName(i)); + if (expBytes == null) return; + + var expr = new CubismExpressionMotion(expBytes); + _expressionManager.StartMotion(expr, true, _userTimeSeconds); + return; + } + } + } + + /// Stops all currently playing motion animations. + public void StopMotion() => _motionManager.StopAllMotions(); + + /// Stops all currently playing expression animations. + public void StopExpression() => _expressionManager.StopAllMotions(); + } +} diff --git a/source_code/Cubism/Framework/CubismModelSettingJson.cs b/source_code/Cubism/Framework/CubismModelSettingJson.cs new file mode 100644 index 0000000..3838a2a --- /dev/null +++ b/source_code/Cubism/Framework/CubismModelSettingJson.cs @@ -0,0 +1,239 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Interface for reading a Cubism model3.json setting file. + /// All accessors return default/empty values when the corresponding + /// data is absent from the JSON. + /// + public interface ICubismModelSetting + { + /// Returns the relative path to the .moc3 file. + string GetModelFileName(); + + /// Returns the number of texture references in the model. + int GetTextureCount(); + + /// Returns the relative path to the texture at the given index. + string GetTextureFileName(int index); + + /// Returns the relative path to the physics3.json file, or null. + string GetPhysicsFileName(); + + /// Returns the relative path to the pose3.json file, or null. + string GetPoseFileName(); + + /// Returns the number of expressions defined in the model. + int GetExpressionCount(); + + /// Returns the display name of the expression at the given index. + string GetExpressionName(int index); + + /// Returns the relative path to the .exp3.json file for the expression at the given index. + string GetExpressionFileName(int index); + + /// Returns the number of motion groups. + int GetMotionGroupCount(); + + /// Returns the name of the motion group at the given index. + string GetMotionGroupName(int index); + + /// Returns the number of motions within the named group. + int GetMotionCount(string groupName); + + /// Returns the relative path to the motion3.json file for the given group and index. + string GetMotionFileName(string groupName, int index); + + /// Returns the relative path to the associated sound file, or null. + string GetMotionSoundFileName(string groupName, int index); + + /// Returns the fade-in time in seconds for the motion, or -1 if not specified. + float GetMotionFadeInTimeValue(string groupName, int index); + + /// Returns the fade-out time in seconds for the motion, or -1 if not specified. + float GetMotionFadeOutTimeValue(string groupName, int index); + + /// Returns the number of parameters in the EyeBlink group. + int GetEyeBlinkParameterCount(); + + /// Returns the parameter ID at the given index within the EyeBlink group. + string GetEyeBlinkParameterId(int index); + + /// Returns the number of parameters in the LipSync group. + int GetLipSyncParameterCount(); + + /// Returns the parameter ID at the given index within the LipSync group. + string GetLipSyncParameterId(int index); + + /// Returns the number of hit area definitions. + int GetHitAreasCount(); + + /// Returns the hit area ID at the given index. + string GetHitAreaId(int index); + + /// Returns the hit area display name at the given index. + string GetHitAreaName(int index); + + /// + /// Returns the Layout section as a dictionary of lowercased key -> float value. + /// Supported keys: width, height, x, y, center_x, center_y, top, bottom, left, right. + /// + Dictionary GetLayoutMap(); + } + + /// + /// JSON parser for the Cubism .model3.json format (Live2D model settings). + /// Implements . + /// + /// Provides typed access to all sections: FileReferences (moc, textures, + /// expressions, motions), Groups (EyeBlink, LipSync), HitAreas, and Layout. + /// All string path values are returned as-is (relative to the model directory); + /// it is the caller's responsibility to resolve them through a file provider. + /// + /// Instances hold a and should be disposed + /// via when no longer needed. + /// + public class CubismModelSettingJson : ICubismModelSetting + { + private readonly JsonDocument _json; + private readonly JsonElement _root; + private readonly JsonElement? _fileRefs; + private readonly JsonElement? _groups; + private readonly JsonElement? _hitAreas; + private readonly JsonElement? _layout; + + /// + /// Parses the given .model3.json bytes. + /// + public CubismModelSettingJson(byte[] jsonBytes) + { + _json = JsonDocument.Parse(jsonBytes); + _root = _json.RootElement; + if (_root.TryGetProperty("FileReferences", out var fr)) + _fileRefs = fr; + if (_root.TryGetProperty("Groups", out var gr)) + _groups = gr; + if (_root.TryGetProperty("HitAreas", out var ha)) + _hitAreas = ha; + if (_root.TryGetProperty("Layout", out var lo)) + _layout = lo; + } + + public string GetModelFileName() => + _fileRefs?.GetProperty("Moc").GetString(); + + public int GetTextureCount() => + _fileRefs?.TryGetProperty("Textures", out var arr) == true ? arr.GetArrayLength() : 0; + + public string GetTextureFileName(int index) => + _fileRefs?.GetProperty("Textures")[index].GetString(); + + public string GetPhysicsFileName() => + TryGetString(_fileRefs, "Physics"); + + public string GetPoseFileName() => + TryGetString(_fileRefs, "Pose"); + + public int GetExpressionCount() => + _fileRefs?.TryGetProperty("Expressions", out var arr) == true ? arr.GetArrayLength() : 0; + + public string GetExpressionName(int index) => + _fileRefs?.GetProperty("Expressions")[index].GetProperty("Name").GetString(); + + public string GetExpressionFileName(int index) => + _fileRefs?.GetProperty("Expressions")[index].GetProperty("File").GetString(); + + public int GetMotionGroupCount() => + _fileRefs?.TryGetProperty("Motions", out var motions) == true ? motions.EnumerateObject().Count() : 0; + + public string GetMotionGroupName(int index) + { + int i = 0; + foreach (var prop in _fileRefs?.GetProperty("Motions").EnumerateObject() ?? default) + if (i++ == index) return prop.Name; + return null; + } + + public int GetMotionCount(string groupName) => + _fileRefs?.TryGetProperty("Motions", out var m) == true && + m.TryGetProperty(groupName, out var grp) ? grp.GetArrayLength() : 0; + + public string GetMotionFileName(string groupName, int index) => + _fileRefs?.GetProperty("Motions").GetProperty(groupName)[index].GetProperty("File").GetString(); + + public string GetMotionSoundFileName(string groupName, int index) => + TryGetString(_fileRefs?.GetProperty("Motions").GetProperty(groupName)[index], "Sound"); + + public float GetMotionFadeInTimeValue(string groupName, int index) => + TryGetFloat(_fileRefs?.GetProperty("Motions").GetProperty(groupName)[index], "FadeInTime"); + + public float GetMotionFadeOutTimeValue(string groupName, int index) => + TryGetFloat(_fileRefs?.GetProperty("Motions").GetProperty(groupName)[index], "FadeOutTime"); + + public int GetEyeBlinkParameterCount() + { + foreach (var group in _groups?.EnumerateArray() ?? default) + if (group.GetProperty("Name").GetString() == "EyeBlink") + return group.GetProperty("Ids").GetArrayLength(); + return 0; + } + + public string GetEyeBlinkParameterId(int index) + { + foreach (var group in _groups?.EnumerateArray() ?? default) + if (group.GetProperty("Name").GetString() == "EyeBlink") + return group.GetProperty("Ids")[index].GetString(); + return null; + } + + public int GetLipSyncParameterCount() + { + foreach (var group in _groups?.EnumerateArray() ?? default) + if (group.GetProperty("Name").GetString() == "LipSync") + return group.GetProperty("Ids").GetArrayLength(); + return 0; + } + + public string GetLipSyncParameterId(int index) + { + foreach (var group in _groups?.EnumerateArray() ?? default) + if (group.GetProperty("Name").GetString() == "LipSync") + return group.GetProperty("Ids")[index].GetString(); + return null; + } + + public int GetHitAreasCount() => + _hitAreas?.GetArrayLength() ?? 0; + + public string GetHitAreaId(int index) => + _hitAreas?[index].GetProperty("Id").GetString(); + + public string GetHitAreaName(int index) => + _hitAreas?[index].GetProperty("Name").GetString(); + + public Dictionary GetLayoutMap() + { + var map = new Dictionary(); + if (_layout == null) return map; + foreach (var prop in _layout.Value.EnumerateObject()) + if (prop.Value.ValueKind == JsonValueKind.Number) + map[prop.Name.ToLowerInvariant()] = prop.Value.GetSingle(); + return map; + } + + private static string TryGetString(JsonElement? parent, string key) => + parent?.TryGetProperty(key, out var val) == true ? val.GetString() : null; + + private static float TryGetFloat(JsonElement? parent, string key) => + parent?.TryGetProperty(key, out var val) == true && val.ValueKind == JsonValueKind.Number + ? val.GetSingle() : -1f; + + /// + /// Releases the underlying . + /// + public void Dispose() => _json?.Dispose(); + } +} diff --git a/source_code/Cubism/Framework/CubismMotion.cs b/source_code/Cubism/Framework/CubismMotion.cs new file mode 100644 index 0000000..f04b8da --- /dev/null +++ b/source_code/Cubism/Framework/CubismMotion.cs @@ -0,0 +1,439 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Cthangover.Live2D.Cubism.Framework +{ + public delegate float MotionSegmentFunction(float[] segments, int baseIndex, float time); + + /// + /// Parses and plays a Cubism .motion3.json animation. + /// Manages three categories of curves: + /// + /// Model curves — eye blink, lip sync, and opacity + /// Parameter curves — arbitrary model parameter animations + /// Part opacity curves — visibility/opacity of model parts + /// + /// + /// Each curve is a sequence of segments (linear, stepped, bezier) stored in + /// flat arrays for performance. Evaluation uses a segment-finding pass + /// followed by per-segment interpolation — linear, stepped, or bezier + /// (analytic via Cardano formula or binary search for restricted beziers). + /// + /// The four-pass update cycle (): + /// + /// Evaluate model curves (eye blink, lip sync, opacity) + /// Evaluate parameter curves, applying eye blink/lip sync modulation + /// Apply eye blink/lip sync to remaining (non-animated) parameters + /// Evaluate part opacity curves + /// + /// + /// Parameter fade-in/out is handled per-curve (overriding the motion-level + /// fade settings). Curve targets and IDs come from the motion3.json metadata. + /// + public class CubismMotion : ACubismMotion + { + private const string ModelCurveIdEyeBlink = "EyeBlink"; + private const string ModelCurveIdLipSync = "LipSync"; + private const string ModelCurveIdOpacity = "Opacity"; + + private readonly float _sourceFrameRate; + private float _lastWeight; + private CubismMotionJson _motionJson; + + private readonly List _modelCurves = new(); + private readonly List _parameterCurves = new(); + private readonly List _partOpacityCurves = new(); + + private readonly List _eyeBlinkParameterIds = new(); + private readonly List _lipSyncParameterIds = new(); + + private readonly List _events = new(); + private readonly List _firedEventValues = new(); + + private float _modelOpacity = 1f; + private bool _areBeziersRestricted; + + /// + /// Parses a .motion3.json byte buffer. + /// Reads the meta section (duration, loop, fps, fade times, bezier flag) + /// and delegates curve/event parsing to private helpers. + /// + public CubismMotion(byte[] motion3JsonBytes) + { + _motionJson = new CubismMotionJson(motion3JsonBytes); + _sourceFrameRate = _motionJson.GetFps(); + _areBeziersRestricted = _motionJson.AreBeziersRestricted(); + _isLoop = _motionJson.IsLoop(); + + if (_motionJson.GetFadeInTime() >= 0f) _fadeInSeconds = _motionJson.GetFadeInTime(); + if (_motionJson.GetFadeOutTime() >= 0f) _fadeOutSeconds = _motionJson.GetFadeOutTime(); + + ParseCurves(); + ParseEvents(); + } + + /// + /// Configures which parameters are affected by eye blink and lip sync + /// modulation. These lists typically come from the model3.json Groups. + /// When a parameter curve targets one of these IDs, its value is + /// multiplied by the eye blink curve value (for blink) or added to + /// the lip sync curve value (for lip sync) in Pass 2. + /// + public void SetEffectIds(List eyeBlinkIds, List lipSyncIds) + { + _eyeBlinkParameterIds.Clear(); + _lipSyncParameterIds.Clear(); + if (eyeBlinkIds != null) _eyeBlinkParameterIds.AddRange(eyeBlinkIds); + if (lipSyncIds != null) _lipSyncParameterIds.AddRange(lipSyncIds); + } + + /// + /// Returns the total duration from the motion3.json, or -1 for looping motions. + /// + public override float GetDuration() => _isLoop ? -1f : _motionJson.GetDuration(); + + /// + /// Returns the full duration even when looping is active — used for computing + /// loop cycle length. + /// + public override float GetLoopDuration() => _motionJson.GetDuration(); + + /// Whether the model's overall opacity is actively controlled by this motion. + public bool IsExistModelOpacity() => _modelOpacity > 0f; + + /// Current model opacity value from the Opacity model curve, clamped [0,1]. + public float GetModelOpacity() => _modelOpacity; + + /// + /// Collects event values that fired between two time points. + /// Clears the internal fired-event list on each call. (Currently returns 0f; + /// the fired values list is populated as a side effect.) + /// + public float GetFiredEventValue(float beforeTime, float motionTime) + { + _firedEventValues.Clear(); + foreach (var evt in _events) + if (evt.FireTime > beforeTime && evt.FireTime <= motionTime) + _firedEventValues.Add(evt.Value); + return 0f; + } + + /// + /// Four-pass parameter update. Called by + /// every frame. + /// + public override unsafe void DoUpdateParameters( + CubismNativeModel model, float userTimeSeconds, float weight, CubismMotionQueueEntry entry) + { + if (model == null || entry == null) return; + + AdjustEndTime(entry, _motionJson.GetDuration()); + + var timeOffset = userTimeSeconds - entry.StartTime; + var duration = _motionJson.GetDuration(); + if (_isLoop && duration > 0f) + { + while (timeOffset > duration) + timeOffset -= duration; + } + + var fadeWeight = weight; + + int paramCount = model.ParameterCount; + var idsPtr = model.GetParameterIds(); + var values = model.GetParameterValues(); + var minValues = model.GetParameterMinimumValues(); + var maxValues = model.GetParameterMaximumValues(); + var defaultValues = model.GetParameterDefaultValues(); + + float eyeBlinkValue = 0f; + float lipSyncValue = 0f; + _modelOpacity = 1f; + var modifiedFlags = new bool[paramCount]; + + // Pass 1 — model curves + foreach (var curve in _modelCurves) + { + var val = EvaluateCurve(curve, timeOffset, fadeWeight); + if (curve.Id == ModelCurveIdEyeBlink) eyeBlinkValue = val; + else if (curve.Id == ModelCurveIdLipSync) lipSyncValue = val; + else if (curve.Id == ModelCurveIdOpacity) _modelOpacity = Math.Clamp(val, 0f, 1f); + } + + // Pass 2 — parameter curves + foreach (var curve in _parameterCurves) + { + var curveValue = EvaluateCurve(curve, timeOffset, fadeWeight); + + for (int pi = 0; pi < paramCount; pi++) + { + var pid = CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi); + if (pid != curve.Id) continue; + + var modulatedValue = curveValue; + if (_eyeBlinkParameterIds.Contains(pid)) + modulatedValue *= eyeBlinkValue; + if (_lipSyncParameterIds.Contains(pid)) + modulatedValue += lipSyncValue; + + var paramFadeWeight = fadeWeight; + if (curve.ParamFadeIn >= 0f || curve.ParamFadeOut >= 0f) + { + var fi = curve.ParamFadeIn >= 0f ? curve.ParamFadeIn : 1f; + var fo = curve.ParamFadeOut >= 0f ? curve.ParamFadeOut : 1f; + var paramFadeIn = CubismMath.GetEasingSine( + (userTimeSeconds - entry.FadeInStartTime) / fi); + var paramFadeOut = CubismMath.GetEasingSine( + (entry.EndTime > 0f ? entry.EndTime - userTimeSeconds : 1f) / fo); + paramFadeWeight = _weight * paramFadeIn * paramFadeOut; + } + + values[pi] = values[pi] + (modulatedValue - values[pi]) * paramFadeWeight; + ClampParameter(pi, values, minValues, maxValues); + modifiedFlags[pi] = true; + break; + } + } + + // Pass 3 — remaining eye blink / lip sync (non-modulated params) + for (int pi = 0; pi < paramCount; pi++) + { + if (modifiedFlags[pi]) continue; + var pid = CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi); + if (_eyeBlinkParameterIds.Contains(pid)) + values[pi] = eyeBlinkValue > 0f ? values[pi] * eyeBlinkValue : values[pi]; + if (_lipSyncParameterIds.Contains(pid)) + values[pi] += lipSyncValue; + } + + // Pass 4 — part opacity curves + var partOpacities = model.GetPartOpacities(); + var partIdsPtr = model.GetPartIds(); + int partCount = model.GetPartCount(); + foreach (var curve in _partOpacityCurves) + { + var val = EvaluateCurve(curve, timeOffset, fadeWeight); + for (int pi = 0; pi < partCount; pi++) + { + var pid = CubismNativeModel.ReadStringFromPtrArray(partIdsPtr, pi); + if (pid == curve.Id) + { + partOpacities[pi] = val; + break; + } + } + } + + // End-of-motion + if (!_isLoop && timeOffset >= duration) + { + entry.IsFinished = true; + OnFinishedMotion?.Invoke(this); + } + else if (_isLoop && timeOffset >= duration) + { + entry.StartTime = userTimeSeconds - (timeOffset - duration); + if (!_isLoopFadeIn) + entry.FadeInStartTime = userTimeSeconds; + } + } + + private float EvaluateCurve(MotionCurveData curve, float time, float weight) + { + var segIdx = FindSegmentIndex(curve, time); + if (segIdx < 0 || segIdx >= curve.SegmentCount) return 0f; + return EvaluateSegment(curve, segIdx, time); + } + + private int FindSegmentIndex(MotionCurveData curve, float time) + { + for (int i = 0; i < curve.SegmentCount; i++) + { + var nextIdx = curve.BaseSegmentIndex + i + 1; + if (nextIdx < curve.Points.Length) + { + var nextPoint = curve.Points[nextIdx]; + if (time < nextPoint.Time) + return i; + } + } + var lastPt = curve.Points[curve.BaseSegmentIndex + curve.SegmentCount]; + if (time <= lastPt.Time) + return curve.SegmentCount - 1; + return -1; + } + + private float EvaluateSegment(MotionCurveData curve, int segIndex, float time) + { + var basePtIdx = curve.BaseSegmentIndex + segIndex; + var p0 = curve.Points[basePtIdx]; + var p1 = curve.Points[basePtIdx + 1]; + + var segType = curve.SegmentTypes != null && segIndex < curve.SegmentTypes.Length + ? curve.SegmentTypes[segIndex] : CubismMotionSegmentType.Linear; + + switch (segType) + { + case CubismMotionSegmentType.Linear: + return CubismMath.LinearEvaluation(ref p0, ref p1, time); + + case CubismMotionSegmentType.Stepped: + return p0.Value; + + case CubismMotionSegmentType.InverseStepped: + return p1.Value; + + case CubismMotionSegmentType.Bezier: + var cp1 = curve.Points[basePtIdx + 1]; + var cp2 = curve.Points[basePtIdx + 2]; + var cp3 = curve.Points[basePtIdx + 3]; + + if (_areBeziersRestricted) + return CubismMath.BezierEvaluateBinarySearch( + p0.Time, cp1.Time, cp2.Time, cp3.Time, + p0.Value, cp1.Value, cp2.Value, cp3.Value, time); + else + { + var t = CubismMath.CardanoAlgorithmForBezier( + p0.Time, cp1.Time, cp2.Time, cp3.Time, time); + t = Math.Clamp(t, 0f, 1f); + var t1 = 1f - t; + var t2 = t * t; + var t12 = t1 * t1; + return t12 * t1 * p0.Value + 3f * t12 * t * cp1.Value + + 3f * t1 * t2 * cp2.Value + t2 * t * cp3.Value; + } + + default: + return CubismMath.LinearEvaluation(ref p0, ref p1, time); + } + } + + private static unsafe void ClampParameter(int index, float* values, float* min, float* max) + { + if (values[index] < min[index]) values[index] = min[index]; + if (values[index] > max[index]) values[index] = max[index]; + } + + private void ParseCurves() + { + var segsFlat = new float[_motionJson.GetTotalSegmentCount() * 2]; + var segTypesFlat = new CubismMotionSegmentType[_motionJson.GetTotalSegmentCount()]; + var allPoints = new List(); + int segOffset = 0; + + for (int ci = 0; ci < _motionJson.GetCurveCount(); ci++) + { + var target = _motionJson.GetCurveTarget(ci); + var id = _motionJson.GetCurveId(ci); + var fadeIn = _motionJson.GetCurveFadeInTime(ci); + var fadeOut = _motionJson.GetCurveFadeOutTime(ci); + _motionJson.GetCurveSegments(ci, out var rawSegments); + + var segmentCount = ParseSegments(rawSegments, segsFlat, segTypesFlat, segOffset, allPoints); + var curveTarget = ParseTarget(target); + + var curveData = new MotionCurveData + { + Type = curveTarget, + Id = id, + SegmentCount = segmentCount, + BaseSegmentIndex = segOffset, + ParamFadeIn = fadeIn, + ParamFadeOut = fadeOut, + Points = allPoints.ToArray(), + SegmentTypes = segTypesFlat + }; + + segOffset += segmentCount; + + switch (curveTarget) + { + case CubismMotionCurveTarget.Model: + _modelCurves.Add(curveData); break; + case CubismMotionCurveTarget.Parameter: + _parameterCurves.Add(curveData); break; + case CubismMotionCurveTarget.PartOpacity: + _partOpacityCurves.Add(curveData); break; + } + } + } + + private int ParseSegments(float[] raw, float[] segsFlat, CubismMotionSegmentType[] segTypesFlat, + int segOffset, List points) + { + int count = 0; + int i = 0; + while (i < raw.Length) + { + var segType = (CubismMotionSegmentType)(int)raw[i++]; + + if (count == 0) + { + // First "segment" is actually the starting point + points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] }); + continue; + } + + segTypesFlat[segOffset + count - 1] = segType; + + switch (segType) + { + case CubismMotionSegmentType.Linear: + case CubismMotionSegmentType.Stepped: + case CubismMotionSegmentType.InverseStepped: + points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] }); + break; + + case CubismMotionSegmentType.Bezier: + points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] }); + points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] }); + points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] }); + break; + } + + count++; + } + + return count - 1; + } + + private static CubismMotionCurveTarget ParseTarget(string target) => target switch + { + "Model" => CubismMotionCurveTarget.Model, + "Parameter" => CubismMotionCurveTarget.Parameter, + "PartOpacity" => CubismMotionCurveTarget.PartOpacity, + _ => CubismMotionCurveTarget.Parameter + }; + + private void ParseEvents() + { + for (int i = 0; i < _motionJson.GetEventCount(); i++) + { + _events.Add(new CubismMotionEvent + { + FireTime = _motionJson.GetEventTime(i), + Value = _motionJson.GetEventValue(i) + }); + } + } + } + + /// + /// Internal data for a single parsed motion curve. + /// Holds the curve type, target ID, segment layout, points array, + /// and optional per-curve fade-in/fade-out durations. + /// + internal class MotionCurveData + { + public CubismMotionCurveTarget Type; + public string Id; + public int SegmentCount; + public int BaseSegmentIndex; + public float ParamFadeIn; + public float ParamFadeOut; + public CubismMotionPoint[] Points; + public CubismMotionSegmentType[] SegmentTypes; + } +} diff --git a/source_code/Cubism/Framework/CubismMotionInternal.cs b/source_code/Cubism/Framework/CubismMotionInternal.cs new file mode 100644 index 0000000..2caf7a3 --- /dev/null +++ b/source_code/Cubism/Framework/CubismMotionInternal.cs @@ -0,0 +1,62 @@ +using System.Runtime.InteropServices; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Specifies which part of the model a motion curve affects. + /// + public enum CubismMotionCurveTarget { Model, Parameter, PartOpacity } + + /// + /// Segment interpolation type within a motion curve. + /// + /// — straight line between two points + /// — cubic bezier (4 control points per segment) + /// — hold previous value until the segment ends + /// — jump to next value at segment start + /// + /// + public enum CubismMotionSegmentType { Linear = 0, Bezier = 1, Stepped = 2, InverseStepped = 3 } + + /// + /// A time-value pair representing a keyframe or control point on a motion curve. + /// + [StructLayout(LayoutKind.Sequential)] + public struct CubismMotionPoint + { + /// Time in seconds from the start of the motion. + public float Time; + + /// Value at this point. For bezier control points, this is an intermediate value. + public float Value; + } + + /// + /// Describes a single curve within a motion. Contains the target type, + /// parameter/part ID, segment count, offset into the flat segment data, + /// and per-curve fade timing. + /// + public struct CubismMotionCurve + { + public CubismMotionCurveTarget Type; + public string Id; + public int SegmentCount; + public int BaseSegmentIndex; + public float FadeInTime; + public float FadeOutTime; + } + + /// + /// A named event that fires at a specific time during motion playback. + /// Events are defined in the UserData section of the .motion3.json. + /// Accessed via . + /// + public struct CubismMotionEvent + { + /// Time offset in seconds when this event triggers. + public float FireTime; + + /// String value associated with the event. + public string Value; + } +} diff --git a/source_code/Cubism/Framework/CubismMotionJson.cs b/source_code/Cubism/Framework/CubismMotionJson.cs new file mode 100644 index 0000000..3566daf --- /dev/null +++ b/source_code/Cubism/Framework/CubismMotionJson.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Text.Json; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// JSON parser for the Cubism .motion3.json format. + /// Provides typed access to the Meta section (duration, loop, fps, fade times, + /// curve statistics, beziers-restricted flag) and the Curves/UserData sections. + /// + /// Parsed curve segments are returned as flat float arrays (interleaved + /// segment-type markers and point data), decoded by . + /// + public class CubismMotionJson + { + private readonly JsonDocument _json; + private readonly JsonElement _meta; + private readonly JsonElement _curves; + private readonly JsonElement _userData; + private readonly bool _hasUserData; + private readonly bool _isLoop; + private readonly float _duration; + private readonly float _fps; + private readonly int _curveCount; + private readonly int _totalSegmentCount; + private readonly int _totalPointCount; + private readonly bool _areBeziersRestricted; + + /// + /// Parses the given .motion3.json bytes and pre-extracts all Meta fields + /// into readonly fields for fast repeated access. + /// + public CubismMotionJson(byte[] jsonBytes) + { + _json = JsonDocument.Parse(jsonBytes); + var root = _json.RootElement; + _meta = root.GetProperty("Meta"); + _curves = root.GetProperty("Curves"); + _hasUserData = root.TryGetProperty("UserData", out _userData); + + _duration = _meta.GetProperty("Duration").GetSingle(); + _isLoop = _meta.TryGetProperty("Loop", out var loop) && loop.GetBoolean(); + _fps = _meta.TryGetProperty("Fps", out var fpsEl) ? fpsEl.GetSingle() : 30f; + _curveCount = _meta.TryGetProperty("CurveCount", out var ccEl) ? ccEl.GetInt32() : 0; + _totalSegmentCount = _meta.TryGetProperty("TotalSegmentCount", out var tscEl) ? tscEl.GetInt32() : 0; + _totalPointCount = _meta.TryGetProperty("TotalPointCount", out var tpcEl) ? tpcEl.GetInt32() : 0; + _areBeziersRestricted = _meta.TryGetProperty("AreBeziersRestricted", out var abrEl) && abrEl.GetBoolean(); + } + + /// Total motion duration in seconds from the Meta section. + public float GetDuration() => _duration; + + /// Whether the motion loops. + public bool IsLoop() => _isLoop; + + /// Frames per second, default 30. + public float GetFps() => _fps; + + /// Number of curves in this motion. + public int GetCurveCount() => _curveCount; + + /// Total segment count across all curves. + public int GetTotalSegmentCount() => _totalSegmentCount; + + /// Total point count across all curves. + public int GetTotalPointCount() => _totalPointCount; + + /// + /// If true, bezier curves use binary search instead of the Cardano formula + /// (non-standard bezier parameterization). + /// + public bool AreBeziersRestricted() => _areBeziersRestricted; + + /// + /// Motion-level fade-in time in seconds, or -1 if not specified. + /// + public float GetFadeInTime() => + _meta.TryGetProperty("FadeInTime", out var v) ? v.GetSingle() : -1f; + + /// + /// Motion-level fade-out time in seconds, or -1 if not specified. + /// + public float GetFadeOutTime() => + _meta.TryGetProperty("FadeOutTime", out var v) ? v.GetSingle() : -1f; + + /// + /// Returns the target string for the curve at the given index + /// ("Model", "Parameter", or "PartOpacity"). + /// + public string GetCurveTarget(int index) => + _curves[index].GetProperty("Target").GetString(); + + /// + /// Returns the parameter/part/model ID string for the curve at the given index. + /// + public string GetCurveId(int index) => + _curves[index].GetProperty("Id").GetString(); + + /// + /// Per-curve fade-in time in seconds, or -1 if not specified. + /// + public float GetCurveFadeInTime(int index) => + _curves[index].TryGetProperty("FadeInTime", out var v) ? v.GetSingle() : -1f; + + /// + /// Per-curve fade-out time in seconds, or -1 if not specified. + /// + public float GetCurveFadeOutTime(int index) => + _curves[index].TryGetProperty("FadeOutTime", out var v) ? v.GetSingle() : -1f; + + /// + /// Reads the raw float array of segment data for a curve. + /// The first element is the segment type marker (cast to int), + /// followed by point data depending on the segment type. + /// + public void GetCurveSegments(int index, out float[] segments) + { + var segArr = _curves[index].GetProperty("Segments"); + segments = new float[segArr.GetArrayLength()]; + int si = 0; + foreach (var s in segArr.EnumerateArray()) + segments[si++] = s.GetSingle(); + } + + /// Number of user-data events in the motion. + public int GetEventCount() => _hasUserData ? _userData.GetArrayLength() : 0; + + /// Fire time of the event at the given index, in seconds. + public float GetEventTime(int index) => + _hasUserData && index < _userData.GetArrayLength() + ? _userData[index].GetProperty("Time").GetSingle() : 0f; + + /// String value of the event at the given index. + public string GetEventValue(int index) => + _hasUserData && index < _userData.GetArrayLength() + ? _userData[index].GetProperty("Value").GetString() : null; + } +} diff --git a/source_code/Cubism/Framework/CubismMotionQueueEntry.cs b/source_code/Cubism/Framework/CubismMotionQueueEntry.cs new file mode 100644 index 0000000..8a6613c --- /dev/null +++ b/source_code/Cubism/Framework/CubismMotionQueueEntry.cs @@ -0,0 +1,163 @@ +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Represents one enqueued motion in the playback queue. + /// Holds timing data (start time, end time, fade-in start time), + /// lifecycle flags (available, finished, auto-delete), and a + /// reference to the motion itself. + /// + /// Created by + /// and consumed frame-by-frame in . + /// + public class CubismMotionQueueEntry + { + /// Absolute user time when motion playback begins. + public float StartTime; + + /// + /// Absolute user time when playback ends. -1 for infinite (looping) motions. + /// + public float EndTime; + + /// Absolute user time when fade-in begins (may differ from StartTime for offset motions). + public float FadeInStartTime; + + /// Whether this entry should be processed during updates. + public bool IsAvailable; + + /// Flag set when playback completes. Checked by the queue manager during cleanup. + public bool IsFinished; + + /// The motion instance driving parameter updates. + public ACubismMotion Motion; + + /// If true, the entry is automatically removed from the queue when finished. + public bool AutoDelete; + + /// Arbitrary user data attached to this queue entry. + public object CustomData; + + /// + /// Creates a new queue entry for the given motion. + /// AutoDelete controls whether the entry is removed on finish. + /// + public CubismMotionQueueEntry(ACubismMotion motion, bool autoDelete) + { + Motion = motion; + AutoDelete = autoDelete; + IsAvailable = true; + } + } + + /// + /// Manages a queue of instances for a single model. + /// Supports priority reservation (higher-priority motions preempt lower ones), + /// concurrent playback of multiple motions, and automatic cleanup of finished entries. + /// + /// Two instances are used by : + /// one for character motions (animations) and one for expressions. + /// Each is updated independently in the per-frame loop, allowing motions + /// and expressions to blend independently with their own fade weights. + /// + /// The update loop iterates entries in reverse for safe removal during + /// traversal. Finished auto-delete entries are removed immediately; + /// non-auto-delete entries remain in the queue with their IsFinished flag set. + /// + public class CubismMotionQueueManager + { + private readonly System.Collections.Generic.List _entries = new(); + private int _reservedPriority = -1; + + /// + /// Sets the reserved priority threshold. + /// Motions with priority below this are rejected by . + /// + public void SetReservePriority(int priority) => _reservedPriority = priority; + + /// + /// Attempts to reserve the given priority level. + /// Returns true if the priority is accepted (higher than current reserved), + /// false if a higher-priority motion is already reserved. + /// + public bool ReserveMotion(int priority) + { + if (_reservedPriority >= 0 && _reservedPriority > priority) + return false; + + _reservedPriority = priority; + return true; + } + + /// + /// Creates and enqueues a motion for playback. + /// Instantiates a , calls + /// on it, and + /// adds it to the queue. Returns the entry for optional tracking. + /// + /// The motion to play. + /// Whether to auto-remove on finish. + /// Current user time for start time calculation. + public CubismMotionQueueEntry StartMotion(ACubismMotion motion, bool autoDelete, float userTimeSeconds) + { + if (motion == null) + return null; + + var entry = new CubismMotionQueueEntry(motion, autoDelete); + motion.SetupMotionQueueEntry(entry, userTimeSeconds); + _entries.Add(entry); + return entry; + } + + /// + /// Stops all motions and clears the queue. Resets priority reservation. + /// + public void StopAllMotions() + { + _entries.Clear(); + _reservedPriority = -1; + } + + /// + /// Returns true if all entries in the queue are finished. + /// + public bool IsFinished() + { + foreach (var entry in _entries) + if (!entry.IsFinished) + return false; + return true; + } + + /// + /// Per-frame update for all queued motions. + /// Iterates in reverse for safe removal. For each active entry: + /// computes fade weight, invokes , + /// and checks for completion. Finished auto-delete entries are removed. + /// + public void DoUpdateMotion(CubismNativeModel model, float userTimeSeconds) + { + for (int i = _entries.Count - 1; i >= 0; i--) + { + var entry = _entries[i]; + if (!entry.IsAvailable || entry.IsFinished) + { + if (entry.AutoDelete) + _entries.RemoveAt(i); + continue; + } + + var weight = entry.Motion.UpdateFadeWeight(entry, userTimeSeconds); + entry.Motion.DoUpdateParameters(model, userTimeSeconds, weight, entry); + + if (entry.Motion.GetDuration() > 0f) + { + if (userTimeSeconds >= entry.EndTime && !entry.Motion.IsLoop) + { + entry.IsFinished = true; + entry.Motion.OnFinishedMotion?.Invoke(entry.Motion); + } + } + } + } + } +} diff --git a/source_code/Cubism/Framework/CubismNativeModel.cs b/source_code/Cubism/Framework/CubismNativeModel.cs new file mode 100644 index 0000000..5558bb2 --- /dev/null +++ b/source_code/Cubism/Framework/CubismNativeModel.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text.Json; +using Cthangover.Core.Utils; +using Godot; +using Cthangover.Live2D.Cubism.Core; +using Cthangover.Core.Mods; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Wraps a Live2D CubismCore native model instance. + /// Handles .moc3 loading with the required 64-byte alignment, exposes + /// model data through direct pointers into CubismCore arrays, and manages + /// texture loading from external file providers. + /// + /// Memory layout: .moc3 data and model instance memory are pinned via + /// GCHandle to satisfy CubismCore's alignment requirements (64-byte and + /// 16-byte respectively). The native model handle (_model) is an opaque + /// pointer passed to all CubismCore P/Invoke calls. + /// + /// Implements to free pinned memory when the + /// model is no longer needed. + /// + public class CubismNativeModel : IDisposable + { + private IntPtr _model; + private GCHandle _mocAlignedHandle; + private GCHandle _modelAlignedHandle; + + private int _drawableCount; + private int _parameterCount; + + /// + /// Array of Godot instances loaded from the + /// model's texture references. Indexed by drawable texture index. + /// + public ImageTexture[] Textures { get; private set; } + + /// Number of drawables (renderable mesh parts) in the model. + public int DrawableCount => _drawableCount; + + /// Number of parameters in the model. + public int ParameterCount => _parameterCount; + + /// Canvas pixel width from the moc3. Available after . + public float CanvasWidth { get; private set; } + + /// Canvas pixel height from the moc3. Available after . + public float CanvasHeight { get; private set; } + + /// Pixels-per-unit scale for converting Cubism coordinates to pixel space. + public float PixelsPerUnit { get; private set; } + + /// Whether the native model has been successfully initialized. + public bool IsLoaded => _model != IntPtr.Zero; + + /// Raw native model handle passed to CubismCore functions. + public IntPtr ModelPtr => _model; + + /// + /// Loads raw .moc3 bytes into CubismCore. + /// Allocates a 64-byte aligned buffer, copies moc data, revives the moc, + /// allocates a 16-byte aligned model buffer, and initializes the model. + /// Throws on failure. + /// + public unsafe void LoadMoc(byte[] mocBytes) + { + int alignedMocSize = (mocBytes.Length + CubismCoreBindings.AlignofMoc - 1) + / CubismCoreBindings.AlignofMoc * CubismCoreBindings.AlignofMoc; + byte[] mocBuffer = new byte[alignedMocSize + CubismCoreBindings.AlignofMoc]; + _mocAlignedHandle = GCHandle.Alloc(mocBuffer, GCHandleType.Pinned); + IntPtr mocPtr = _mocAlignedHandle.AddrOfPinnedObject(); + long alignedAddr = (mocPtr.ToInt64() + CubismCoreBindings.AlignofMoc - 1) + / CubismCoreBindings.AlignofMoc * CubismCoreBindings.AlignofMoc; + var alignedMocPtr = new IntPtr(alignedAddr); + Marshal.Copy(mocBytes, 0, alignedMocPtr, mocBytes.Length); + + IntPtr moc = CubismCoreBindings.csmReviveMocInPlace( + (void*)alignedMocPtr, (uint)mocBytes.Length); + if (moc == IntPtr.Zero) + { + GameLogger.Log("LIVE2D", "LoadMoc: csmReviveMocInPlace failed — .moc3 data is invalid or corrupted", LogLevel.Error); + throw new InvalidOperationException("csmReviveMocInPlace failed"); + } + + uint modelSize = CubismCoreBindings.csmGetSizeofModel(moc); + byte[] modelBuffer = new byte[modelSize + CubismCoreBindings.AlignofModel]; + _modelAlignedHandle = GCHandle.Alloc(modelBuffer, GCHandleType.Pinned); + IntPtr modelPtr = _modelAlignedHandle.AddrOfPinnedObject(); + long modelAlignedAddr = (modelPtr.ToInt64() + CubismCoreBindings.AlignofModel - 1) + / CubismCoreBindings.AlignofModel * CubismCoreBindings.AlignofModel; + var alignedModelPtr = new IntPtr(modelAlignedAddr); + + _model = CubismCoreBindings.csmInitializeModelInPlace( + moc, (void*)alignedModelPtr, modelSize); + if (_model == IntPtr.Zero) + { + GameLogger.Log("LIVE2D", "LoadMoc: csmInitializeModelInPlace failed — model memory allocation failed", LogLevel.Error); + throw new InvalidOperationException("csmInitializeModelInPlace failed"); + } + + var sizeInPixels = default(CubismCoreBindings.csmVector2); + var originInPixels = default(CubismCoreBindings.csmVector2); + float ppu = 0; + CubismCoreBindings.csmReadCanvasInfo(_model, &sizeInPixels, &originInPixels, &ppu); + CanvasWidth = sizeInPixels.X; + CanvasHeight = sizeInPixels.Y; + PixelsPerUnit = ppu; + + _drawableCount = CubismCoreBindings.csmGetDrawableCount(_model); + _parameterCount = CubismCoreBindings.csmGetParameterCount(_model); + } + + /// + /// Loads a model from .model3.json bytes. + /// Parses the FileReferences section to find the .moc3 file, + /// calls , then loads all referenced textures + /// through the given file provider. + /// + public void LoadFromProvider(byte[] model3JsonBytes, string modelDir, IModFileProvider provider) + { + var json = JsonDocument.Parse(model3JsonBytes); + var fileRefs = json.RootElement.GetProperty("FileReferences"); + + string mocRelPath = fileRefs.GetProperty("Moc").GetString(); + var mocBytes = ReadFile(modelDir, mocRelPath, provider); + if (mocBytes == null) + { + GameLogger.Log("LIVE2D", $"LoadFromProvider: .moc3 not found '{mocRelPath}'", LogLevel.Error); + return; + } + LoadMoc(mocBytes); + + var texturePaths = new List(); + if (fileRefs.TryGetProperty("Textures", out var texArr)) + { + foreach (var t in texArr.EnumerateArray()) + texturePaths.Add(t.GetString()); + } + LoadTextures(texturePaths, modelDir, provider); + } + + private static byte[] ReadFile(string baseDir, string relativePath, IModFileProvider provider) + { + var normalized = relativePath.Replace('\\', '/'); + var combined = string.IsNullOrEmpty(baseDir) ? normalized : $"{baseDir}/{normalized}"; + var result = provider.ReadFileBinary(combined); + if (result == null) + GameLogger.Log("LIVE2D", $"ReadFile: provider returned null for '{combined}'", LogLevel.Warning); + return result; + } + + private void LoadTextures(List paths, string modelDir, IModFileProvider provider) + { + Textures = new ImageTexture[paths.Count]; + for (int i = 0; i < paths.Count; i++) + { + var texBytes = ReadFile(modelDir, paths[i], provider); + if (texBytes == null || texBytes.Length == 0) + { + GameLogger.Log("LIVE2D", $"LoadTextures: failed to read texture '{paths[i]}'", LogLevel.Warning); + continue; + } + var img = new Image(); + Error err; + if (texBytes[0] == 0x89 && texBytes[1] == 0x50) + err = img.LoadPngFromBuffer(texBytes); + else + err = img.LoadJpgFromBuffer(texBytes); + + if (err != Error.Ok) + { + GameLogger.Log("LIVE2D", $"LoadTextures: failed to decode '{paths[i]}' — error={err}", LogLevel.Warning); + continue; + } + + if (img.GetFormat() != Image.Format.Rgba8) + img.Convert(Image.Format.Rgba8); + + img.GenerateMipmaps(); + + var tex = ImageTexture.CreateFromImage(img); + Textures[i] = tex; + var w = img.GetWidth(); + var h = img.GetHeight(); + img.Dispose(); + GameLogger.Log("LIVE2D", $"LoadTextures: loaded '{paths[i]}' ({texBytes.Length}b, {w}x{h})"); + } + } + + /// + /// Calls csmUpdateModel on the native model. + /// Must be called after modifying parameters/parts/opacities and before + /// reading vertex positions for rendering. + /// + public unsafe void UpdateModel() + { + if (_model == IntPtr.Zero) return; + CubismCoreBindings.csmUpdateModel(_model); + } + + /// + /// Resets the per-frame dynamic flags for all drawables. + /// Called at the start of each render update cycle. + /// + public unsafe void ResetDynamicFlags() + { + if (_model == IntPtr.Zero) return; + CubismCoreBindings.csmResetDrawableDynamicFlags(_model); + } + + /// Returns the drawable count from the native model. + public unsafe int GetDrawableCount() => _drawableCount; + + /// Returns a pointer to the dynamic flags byte array (one byte per drawable). + public unsafe byte* GetDynamicFlags() => + CubismCoreBindings.csmGetDrawableDynamicFlags(_model); + + /// Returns a pointer to the constant flags byte array (blend mode, double-sided, etc.). + public unsafe byte* GetConstantFlags() => + CubismCoreBindings.csmGetDrawableConstantFlags(_model); + + /// Returns a pointer to the texture index array (one int per drawable). + public unsafe int* GetTextureIndices() => + CubismCoreBindings.csmGetDrawableTextureIndices(_model); + + /// Returns a pointer to the vertex count array (one int per drawable). + public unsafe int* GetVertexCounts() => + CubismCoreBindings.csmGetDrawableVertexCounts(_model); + + /// Returns a pointer array of csmVector2 arrays — vertex positions per drawable. + public unsafe CubismCoreBindings.csmVector2** GetVertexPositions() => + CubismCoreBindings.csmGetDrawableVertexPositions(_model); + + /// Returns a pointer array of csmVector2 arrays — UV coordinates per drawable. + public unsafe CubismCoreBindings.csmVector2** GetVertexUvs() => + CubismCoreBindings.csmGetDrawableVertexUvs(_model); + + /// Returns a pointer to the index count array (one int per drawable). + public unsafe int* GetIndexCounts() => + CubismCoreBindings.csmGetDrawableIndexCounts(_model); + + /// Returns a pointer array of ushort arrays — triangle indices per drawable. + public unsafe ushort** GetIndices() => + CubismCoreBindings.csmGetDrawableIndices(_model); + + /// Returns a pointer to the per-drawable opacity array (one float per drawable). + public unsafe float* GetOpacities() => + CubismCoreBindings.csmGetDrawableOpacities(_model); + + /// Returns a pointer to the draw order indices array. + public unsafe int* GetDrawOrders() => + CubismCoreBindings.csmGetDrawableDrawOrders(_model); + + /// Returns a pointer to the render order indices array. + public unsafe int* GetRenderOrders() => + CubismCoreBindings.csmGetRenderOrders(_model); + + /// Returns a pointer to the mask count array (one int per drawable). + public unsafe int* GetMaskCounts() => + CubismCoreBindings.csmGetDrawableMaskCounts(_model); + + /// Returns a pointer array of int arrays — mask drawable indices per drawable. + public unsafe int** GetMasks() => + CubismCoreBindings.csmGetDrawableMasks(_model); + + /// Returns a pointer to the per-drawable multiply color array. + public unsafe CubismCoreBindings.csmVector4* GetMultiplyColors() => + CubismCoreBindings.csmGetDrawableMultiplyColors(_model); + + /// Returns a pointer to the per-drawable screen color array. + public unsafe CubismCoreBindings.csmVector4* GetScreenColors() => + CubismCoreBindings.csmGetDrawableScreenColors(_model); + + /// Returns a pointer to the current parameter values array (one float per parameter). + public unsafe float* GetParameterValues() => + CubismCoreBindings.csmGetParameterValues(_model); + + /// Returns a pointer to the parameter minimum bounds array. + public unsafe float* GetParameterMinimumValues() => + CubismCoreBindings.csmGetParameterMinimumValues(_model); + + /// Returns a pointer to the parameter maximum bounds array. + public unsafe float* GetParameterMaximumValues() => + CubismCoreBindings.csmGetParameterMaximumValues(_model); + + /// Returns a pointer to the parameter default values array. + public unsafe float* GetParameterDefaultValues() => + CubismCoreBindings.csmGetParameterDefaultValues(_model); + + /// + /// Returns a pointer array of null-terminated ANSI strings — parameter IDs. + /// Use to read individual strings. + /// + public unsafe IntPtr GetParameterIds() => + CubismCoreBindings.csmGetParameterIds(_model); + + /// + /// Reads a null-terminated ANSI string from a pointer array at the given index. + /// Each element is a byte pointer; reads until null terminator. + /// + public static unsafe string ReadStringFromPtrArray(IntPtr ptrArray, int index) + { + var strPtr = *(byte**)IntPtr.Add(ptrArray, index * IntPtr.Size); + return Marshal.PtrToStringAnsi((IntPtr)strPtr); + } + + /// Returns the number of parts from the native model. + public unsafe int GetPartCount() => + CubismCoreBindings.csmGetPartCount(_model); + + /// Returns a pointer to the per-part opacity array (one float per part). + public unsafe float* GetPartOpacities() => + CubismCoreBindings.csmGetPartOpacities(_model); + + /// Returns a pointer array of part ID strings. + public unsafe IntPtr GetPartIds() => + CubismCoreBindings.csmGetPartIds(_model); + + /// + /// Frees pinned memory handles. The native model handle is set to zero; + /// the underlying CubismCore memory was allocated inline and lives in + /// the pinned buffers — freeing those handles releases all model memory. + /// + public void Dispose() + { + if (_mocAlignedHandle.IsAllocated) _mocAlignedHandle.Free(); + if (_modelAlignedHandle.IsAllocated) _modelAlignedHandle.Free(); + _model = IntPtr.Zero; + } + } +} diff --git a/source_code/Cubism/Framework/CubismParameter.cs b/source_code/Cubism/Framework/CubismParameter.cs new file mode 100644 index 0000000..7f2994f --- /dev/null +++ b/source_code/Cubism/Framework/CubismParameter.cs @@ -0,0 +1,50 @@ +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Cubism model parameter. + /// Each parameter has a string ID, a numeric index into the native + /// parameter arrays, current value, and bounds (min, max, default). + /// + /// Parameters are created during + /// and exposed via . External code + /// can read/write and call + /// to push changes back to the CubismCore native arrays. + /// + public class CubismParameter + { + public CubismNativeModel Model { get; } + + public CubismParameter(CubismNativeModel model) + { + Model = model; + } + + /// Cubism parameter ID string (e.g. "ParamAngleX", "ParamMouthOpenY"). + public string Id { get; set; } + + /// Zero-based index into the native parameter arrays. + public int Index { get; set; } + + /// Current parameter value. Writes to native model when is called. + public float Value { get; set; } + + /// Minimum allowed value for this parameter. + public float MinimumValue { get; set; } + + /// Maximum allowed value for this parameter. + public float MaximumValue { get; set; } + + /// Default (rest) value for this parameter from the moc3. + public float DefaultValue { get; set; } + + /// + /// Writes directly to the native model's parameter array. + /// No clamping is performed — the caller should enforce bounds if needed. + /// + public unsafe void ApplyToModel() + { + var values = Model.GetParameterValues(); + values[Index] = Value; + } + } +} diff --git a/source_code/Cubism/Framework/CubismPartOpacity.cs b/source_code/Cubism/Framework/CubismPartOpacity.cs new file mode 100644 index 0000000..bd54856 --- /dev/null +++ b/source_code/Cubism/Framework/CubismPartOpacity.cs @@ -0,0 +1,43 @@ +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Cubism model part opacity. + /// Each part has a string ID, a numeric index into the native part arrays, + /// and a current opacity value (0 = hidden, 1 = fully visible). + /// + /// Parts are created during + /// and exposed via . + /// External code can set and call + /// to push the change to CubismCore. + /// + public class CubismPartOpacity + { + public CubismNativeModel Model { get; } + + public CubismPartOpacity(CubismNativeModel model) + { + Model = model; + } + + /// Cubism part ID string (e.g. "PartArmL", "PartHairBack"). + public string Id { get; set; } + + /// Zero-based index into the native part arrays. + public int Index { get; set; } + + /// + /// Current opacity value for this part. 0 = hidden, 1 = fully visible. + /// Written to native model when is called. + /// + public float Value { get; set; } + + /// + /// Writes directly to the native model's part opacities array. + /// + public unsafe void ApplyToModel() + { + var opacities = Model.GetPartOpacities(); + opacities[Index] = Value; + } + } +} diff --git a/source_code/Cubism/Framework/CubismPhysics.cs b/source_code/Cubism/Framework/CubismPhysics.cs new file mode 100644 index 0000000..442b524 --- /dev/null +++ b/source_code/Cubism/Framework/CubismPhysics.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Cubism physics simulation engine. Drives secondary motion (hair, cloth, + /// accessories) using a chain of particles influenced by parameter inputs + /// (X, Y, angle from model parameters), gravity, and wind. + /// + /// Architecture: + /// + /// holds the complete rig structure: + /// sub-rigs, inputs, outputs, and particles + /// Each sub-rig is an independent chain of particles anchored to one + /// or more model parameters via inputs + /// Inputs map model parameters (normalized and weighted) to particle + /// chain root position/angle + /// Outputs map particle chain state back to model parameters after + /// physics integration + /// + /// + /// The simulation runs at the rig's configured FPS (from .physics3.json), + /// using sub-frame accumulation when the display delta time is smaller + /// than the physics step. Output values are interpolated between steps + /// for smooth visual results. + /// + public class CubismPhysics + { + private const float AirResistance = 5f; + private const float MaximumWeight = 100f; + + private readonly CubismPhysicsRig _rig = new(); + private readonly List _currentOutputs = new(); + private readonly List _previousOutputs = new(); + private float _currentRemainTime; + private float _physicsDeltaTime; + + /// + /// Parses a .physics3.json byte buffer and builds the full physics rig + /// with sub-rigs, inputs, outputs, and particles. + /// + public CubismPhysics(byte[] physics3JsonBytes) + { + var json = new CubismPhysicsJson(physics3JsonBytes); + + json.GetGravity(out _rig.GravityX, out _rig.GravityY); + json.GetWind(out _rig.WindX, out _rig.WindY); + _rig.Fps = json.GetFps(); + _physicsDeltaTime = 1f / _rig.Fps; + + int inputOffset = 0, outputOffset = 0, particleOffset = 0; + for (int si = 0; si < json.GetSubRigCount(); si++) + { + var subRig = new CubismPhysicsSubRig + { + InputCount = json.GetInputCount(si), + OutputCount = json.GetOutputCount(si), + ParticleCount = json.GetVertexCount(si), + BaseInputIndex = inputOffset, + BaseOutputIndex = outputOffset, + BaseParticleIndex = particleOffset + }; + json.GetNormalizationPosition(si, out subRig.NormalizationPosition); + json.GetNormalizationAngle(si, out subRig.NormalizationAngle); + _rig.SubRigs.Add(subRig); + + for (int ii = 0; ii < subRig.InputCount; ii++) + { + json.GetInput(si, ii, out var pid, out var w, out var type, out var reflect); + _rig.Inputs.Add(new CubismPhysicsInput + { + ParameterId = pid, Weight = w, Type = type, Reflect = reflect + }); + } + + for (int oi = 0; oi < subRig.OutputCount; oi++) + { + json.GetOutput(si, oi, out var pid, out var vi, out var sx, out var sy, + out var w, out var type, out var reflect); + _rig.Outputs.Add(new CubismPhysicsOutput + { + ParameterId = pid, VertexIndex = vi + particleOffset, + TranslationScaleX = sx, TranslationScaleY = sy, Weight = w, + Type = type, Reflect = reflect + }); + } + + int totalParticles = particleOffset + subRig.ParticleCount; + var newParticles = new CubismPhysicsParticle[totalParticles]; + if (_rig.Particles.Length > 0) + System.Array.Copy(_rig.Particles, newParticles, _rig.Particles.Length); + _rig.Particles = newParticles; + + for (int pi = 0; pi < subRig.ParticleCount; pi++) + { + var pIdx = particleOffset + pi; + json.GetParticle(si, pi, out var mob, out var del, out var acc, out var rad, + out var px, out var py); + _rig.Particles[pIdx] = new CubismPhysicsParticle + { + InitialX = px, InitialY = py, + Mobility = mob, Delay = del, Acceleration = acc, Radius = rad, + PositionX = px, PositionY = py, + LastPositionX = px, LastPositionY = py + }; + } + + inputOffset += subRig.InputCount; + outputOffset += subRig.OutputCount; + particleOffset += subRig.ParticleCount; + } + + _currentOutputs.Capacity = _rig.Outputs.Count; + _previousOutputs.Capacity = _rig.Outputs.Count; + for (int i = 0; i < _rig.Outputs.Count; i++) + { + _currentOutputs.Add(0f); + _previousOutputs.Add(0f); + } + } + + /// + /// Overrides gravity and wind at runtime. + /// + public void SetOptions(float gravityX, float gravityY, float windX, float windY) + { + _rig.GravityX = gravityX; + _rig.GravityY = gravityY; + _rig.WindX = windX; + _rig.WindY = windY; + } + + /// + /// Forces the physics to stabilize immediately by running one update + /// cycle in stabilization mode. Particles snap to their rest positions + /// along the chain direction. + /// + public void Stabilization(CubismNativeModel model) + { + UpdatePhysics(model, true); + } + + /// + /// Evaluates physics for one display frame. + /// Accumulates time and runs physics steps at the rig's native FPS. + /// Output values are linearly interpolated between the previous and + /// current step for smooth visual results. + /// Called from every frame. + /// + public unsafe void Evaluate(CubismNativeModel model, float deltaTimeSeconds) + { + _currentRemainTime += deltaTimeSeconds; + var physicsDeltaCalc = _physicsDeltaTime > 0f ? _physicsDeltaTime : deltaTimeSeconds; + + var prevValues = model.GetParameterValues(); + var paramCache = stackalloc float[_rig.Inputs.Count]; + for (int i = 0; i < _rig.Inputs.Count; i++) + paramCache[i] = 0f; + + while (_currentRemainTime >= physicsDeltaCalc) + { + _currentRemainTime -= physicsDeltaCalc; + + for (int i = 0; i < _rig.Outputs.Count; i++) + { + _previousOutputs[i] = _currentOutputs[i]; + _currentOutputs[i] = 0f; + } + + UpdatePhysics(model, false); + + for (int i = 0; i < _rig.Outputs.Count; i++) + _currentOutputs[i] = EvaluateOutputValue(i); + } + + var alpha = physicsDeltaCalc > 0f ? _currentRemainTime / physicsDeltaCalc : 0f; + var idsPtr = model.GetParameterIds(); + var values = model.GetParameterValues(); + var maxValues = model.GetParameterMaximumValues(); + var minValues = model.GetParameterMinimumValues(); + + for (int oi = 0; oi < _rig.Outputs.Count; oi++) + { + var output = _rig.Outputs[oi]; + var interpolated = _previousOutputs[oi] * (1f - alpha) + _currentOutputs[oi] * alpha; + for (int pi = 0; pi < model.ParameterCount; pi++) + { + if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == output.ParameterId) + { + var result = interpolated * MaximumWeight; + if (result < minValues[pi]) result = minValues[pi]; + if (result > maxValues[pi]) result = maxValues[pi]; + values[pi] = values[pi] * (1f - output.Weight) + result * output.Weight; + break; + } + } + } + } + + private unsafe void UpdatePhysics(CubismNativeModel model, bool stabilization) + { + var idsPtr = model.GetParameterIds(); + var values = model.GetParameterValues(); + var minValues = model.GetParameterMinimumValues(); + var maxValues = model.GetParameterMaximumValues(); + var defaultValues = model.GetParameterDefaultValues(); + + foreach (var subRig in _rig.SubRigs) + { + float totalTranslationX = 0f, totalTranslationY = 0f, totalAngle = 0f; + + for (int ii = 0; ii < subRig.InputCount; ii++) + { + var input = _rig.Inputs[subRig.BaseInputIndex + ii]; + var paramValue = GetParameterValue(idsPtr, values, model.ParameterCount, input.ParameterId); + + float tx = 0f, ty = 0f, angle = 0f; + switch (input.Type) + { + case CubismPhysicsSource.X: + tx = NormalizeParameterValue(paramValue, subRig.NormalizationPosition, input.Reflect) * input.Weight; + break; + case CubismPhysicsSource.Y: + ty = NormalizeParameterValue(paramValue, subRig.NormalizationPosition, input.Reflect) * input.Weight; + break; + case CubismPhysicsSource.Angle: + angle = NormalizeParameterValue(paramValue, subRig.NormalizationAngle, input.Reflect) * input.Weight; + break; + } + + totalTranslationX += tx; + totalTranslationY += ty; + totalAngle += angle; + } + + var particles = _rig.Particles; + int baseP = subRig.BaseParticleIndex; + + float prevRootX = particles[baseP].PositionX; + float prevRootY = particles[baseP].PositionY; + float smoothRate = MathF.Min(1f, _physicsDeltaTime * 12f); + particles[baseP].PositionX = prevRootX + (totalTranslationX - prevRootX) * smoothRate; + particles[baseP].PositionY = prevRootY + (totalTranslationY - prevRootY) * smoothRate; + + for (int pi = 1; pi < subRig.ParticleCount; pi++) + { + var pIdx = baseP + pi; + var prev = particles[pIdx - 1]; + + if (stabilization) + { + var dirX = particles[pIdx].InitialX - prev.InitialX; + var dirY = particles[pIdx].InitialY - prev.InitialY; + var dist = MathF.Sqrt(dirX * dirX + dirY * dirY); + if (dist > 0f) + { + dirX /= dist; + dirY /= dist; + } + particles[pIdx].PositionX = particles[pIdx - 1].PositionX + dirX * particles[pIdx].Radius; + particles[pIdx].PositionY = particles[pIdx - 1].PositionY + dirY * particles[pIdx].Radius; + } + else + { + var forceX = _rig.GravityX * particles[pIdx].Acceleration + _rig.WindX; + var forceY = _rig.GravityY * particles[pIdx].Acceleration + _rig.WindY; + + var delay = particles[pIdx].Delay * _physicsDeltaTime * 30f; + if (delay > 0f) + { + var delay2 = delay * delay; + var newX = particles[pIdx].PositionX + particles[pIdx].VelocityX * delay + 0.5f * forceX * delay2; + var newY = particles[pIdx].PositionY + particles[pIdx].VelocityY * delay + 0.5f * forceY * delay2; + + var radian = CubismMath.DirectionToRadian( + particles[pIdx].LastGravityX, particles[pIdx].LastGravityY, forceX, forceY); + radian /= AirResistance; + + var dirX = newX - particles[pIdx - 1].PositionX; + var dirY = newY - particles[pIdx - 1].PositionY; + var currentDist = MathF.Sqrt(dirX * dirX + dirY * dirY); + if (currentDist > 0f) + { + var ndirX = dirX / currentDist; + var ndirY = dirY / currentDist; + var cosR = MathF.Cos(radian); + var sinR = MathF.Sin(radian); + dirX = ndirX * cosR - ndirY * sinR; + dirY = ndirX * sinR + ndirY * cosR; + } + particles[pIdx].VelocityX = (newX - particles[pIdx].LastPositionX) / delay * particles[pIdx].Mobility; + particles[pIdx].VelocityY = (newY - particles[pIdx].LastPositionY) / delay * particles[pIdx].Mobility; + } + + particles[pIdx].LastGravityX = forceX; + particles[pIdx].LastGravityY = forceY; + particles[pIdx].ForceX = forceX; + particles[pIdx].ForceY = forceY; + + var curDirX = particles[pIdx].PositionX - particles[pIdx - 1].PositionX; + var curDirY = particles[pIdx].PositionY - particles[pIdx - 1].PositionY; + var curDist = MathF.Sqrt(curDirX * curDirX + curDirY * curDirY); + if (curDist > 0f) + { + curDirX = curDirX / curDist * particles[pIdx].Radius; + curDirY = curDirY / curDist * particles[pIdx].Radius; + } + particles[pIdx].PositionX = particles[pIdx - 1].PositionX + curDirX; + particles[pIdx].PositionY = particles[pIdx - 1].PositionY + curDirY; + particles[pIdx].LastPositionX = particles[pIdx].PositionX; + particles[pIdx].LastPositionY = particles[pIdx].PositionY; + } + } + + for (int oi = 0; oi < subRig.OutputCount; oi++) + { + var outIdx = subRig.BaseOutputIndex + oi; + var output = _rig.Outputs[outIdx]; + var particle = particles[output.VertexIndex]; + var value = EvaluateOutputValueStatic(particle, particles, output, baseP, _currentOutputs[outIdx]); + _currentOutputs[outIdx] = value * output.Weight; + } + } + } + + private float EvaluateOutputValue(int outputIndex) + { + return _currentOutputs[outputIndex]; + } + + private static float EvaluateOutputValueStatic(CubismPhysicsParticle particle, + CubismPhysicsParticle[] particles, CubismPhysicsOutput output, int baseParticleIndex, float currentValue) + { + var pIdx = output.VertexIndex; + var p = particles[pIdx]; + var prev = pIdx > baseParticleIndex ? particles[pIdx - 1] : particles[pIdx]; + + return output.Type switch + { + CubismPhysicsSource.X => p.PositionX - prev.PositionX, + CubismPhysicsSource.Y => p.PositionY - prev.PositionY, + CubismPhysicsSource.Angle => + MathF.Atan2(p.PositionY - prev.PositionY, p.PositionX - prev.PositionX) * 180f / MathF.PI, + _ => p.PositionX + }; + } + + private static unsafe float GetParameterValue(IntPtr idsPtr, float* values, int count, string id) + { + for (int i = 0; i < count; i++) + if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, i) == id) + return values[i]; + return 0f; + } + + private static float NormalizeParameterValue(float value, CubismPhysicsNormalization norm, bool reflect) + { + var range = norm.Maximum - norm.Minimum; + if (MathF.Abs(range) < 1e-6f) return 0f; + var result = (value - norm.Default) / range; + return reflect ? -result : result; + } + } +} diff --git a/source_code/Cubism/Framework/CubismPhysicsJson.cs b/source_code/Cubism/Framework/CubismPhysicsJson.cs new file mode 100644 index 0000000..f5220a1 --- /dev/null +++ b/source_code/Cubism/Framework/CubismPhysicsJson.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Physics input/output dimensionality. + /// + public enum CubismPhysicsSource { X, Y, Angle } + + /// + /// Normalization range for mapping raw parameter values to normalized physics inputs. + /// + public struct CubismPhysicsNormalization { public float Minimum; public float Maximum; public float Default; } + + /// + /// A single particle in a physics chain. Holds position, velocity, force accumulators, + /// and per-particle properties (mobility, delay, acceleration, radius) from the + /// .physics3.json file. Positions are computed relative to the chain anchor. + /// + public struct CubismPhysicsParticle + { + public float InitialX, InitialY; + public float Mobility; + public float Delay; + public float Acceleration; + public float Radius; + public float PositionX, PositionY; + public float LastPositionX, LastPositionY; + public float LastGravityX, LastGravityY; + public float ForceX, ForceY; + public float VelocityX, VelocityY; + } + + /// + /// A sub-rig groups a contiguous range of inputs, outputs, and particles + /// that form one independent physics chain. Each sub-rig has its own + /// normalization settings for position and angle inputs. + /// + public struct CubismPhysicsSubRig + { + public int InputCount, OutputCount, ParticleCount; + public int BaseInputIndex, BaseOutputIndex, BaseParticleIndex; + public CubismPhysicsNormalization NormalizationPosition; + public CubismPhysicsNormalization NormalizationAngle; + } + + /// + /// Maps a model parameter to a physics chain input (X, Y, or angle) + /// with a configurable weight and optional reflection. + /// + public struct CubismPhysicsInput + { + public string ParameterId; + public int SourceParameterIndex; + public float Weight; + public CubismPhysicsSource Type; + public bool Reflect; + } + + /// + /// Maps a physics chain output back to a model parameter. + /// Defines which particle vertex to read, the output type (X, Y, angle), + /// translation scale, weight, and reflection. + /// + public struct CubismPhysicsOutput + { + public string ParameterId; + public int DestinationParameterIndex; + public int VertexIndex; + public float TranslationScaleX, TranslationScaleY; + public float AngleScale; + public float Weight; + public CubismPhysicsSource Type; + public bool Reflect; + public float ValueBelowMinimum; + public float ValueExceededMaximum; + } + + /// + /// Complete physics rig data structure. Holds all sub-rigs, flattened + /// input/output/particle arrays, and global gravity/wind/fps settings. + /// Used by as its internal state. + /// + public class CubismPhysicsRig + { + public List SubRigs = new(); + public List Inputs = new(); + public List Outputs = new(); + public CubismPhysicsParticle[] Particles = System.Array.Empty(); + public float GravityX, GravityY; + public float WindX, WindY; + public float Fps; + } + + /// + /// JSON parser for the Cubism .physics3.json format. + /// Reads the Meta section (fps, gravity, wind) and the PhysicsSettings + /// array (sub-rigs with their inputs, outputs, and vertices/particles). + /// + /// All data is accessed through typed out-parameter methods that + /// parse JSON elements directly — avoiding intermediate allocation + /// of temporary data structures. + /// + public class CubismPhysicsJson + { + private readonly JsonDocument _json; + + /// + /// Parses the given .physics3.json bytes. + /// + public CubismPhysicsJson(byte[] jsonBytes) + { + _json = JsonDocument.Parse(jsonBytes); + } + + /// Physics simulation FPS from the Meta section, default 30. + public float GetFps() + { + var meta = _json.RootElement.GetProperty("Meta"); + return meta.TryGetProperty("Fps", out var v) ? v.GetSingle() : 30f; + } + + /// Number of sub-rig entries in PhysicsSettings. + public int GetSubRigCount() => + _json.RootElement.TryGetProperty("PhysicsSettings", out var arr) ? arr.GetArrayLength() : 0; + + /// Reads gravity vector from Meta.EffectiveForces.Gravity. Defaults to (0, -1). + public void GetGravity(out float x, out float y) + { + x = 0; y = -1; + var meta = _json.RootElement.GetProperty("Meta"); + if (meta.TryGetProperty("EffectiveForces", out var ef) && + ef.TryGetProperty("Gravity", out var g)) + { + x = g.TryGetProperty("X", out var gx) ? gx.GetSingle() : 0f; + y = g.TryGetProperty("Y", out var gy) ? gy.GetSingle() : -1f; + } + } + + /// Reads wind vector from Meta.EffectiveForces.Wind. Defaults to (0, 0). + public void GetWind(out float x, out float y) + { + x = 0; y = 0; + var meta = _json.RootElement.GetProperty("Meta"); + if (meta.TryGetProperty("EffectiveForces", out var ef) && + ef.TryGetProperty("Wind", out var w)) + { + x = w.TryGetProperty("X", out var wx) ? wx.GetSingle() : 0f; + y = w.TryGetProperty("Y", out var wy) ? wy.GetSingle() : 0f; + } + } + + /// Number of input entries in the given sub-rig. + public int GetInputCount(int subRigIndex) => + GetSubRig(subRigIndex).GetProperty("Input").GetArrayLength(); + + /// Number of output entries in the given sub-rig. + public int GetOutputCount(int subRigIndex) => + GetSubRig(subRigIndex).GetProperty("Output").GetArrayLength(); + + /// Number of vertex/particle entries in the given sub-rig. + public int GetVertexCount(int subRigIndex) => + GetSubRig(subRigIndex).GetProperty("Vertices").GetArrayLength(); + + /// Reads position normalization (min, max, default) for the given sub-rig. + public void GetNormalizationPosition(int subRigIndex, out CubismPhysicsNormalization norm) + { + norm = default; + var sr = GetSubRig(subRigIndex); + if (!sr.TryGetProperty("Normalization", out var n)) return; + if (!n.TryGetProperty("Position", out var pos)) return; + norm.Minimum = pos.TryGetProperty("Minimum", out var p) ? p.GetSingle() : -30f; + norm.Maximum = pos.TryGetProperty("Maximum", out var pp) ? pp.GetSingle() : 30f; + norm.Default = pos.TryGetProperty("Default", out var pd) ? pd.GetSingle() : 0f; + } + + /// Reads angle normalization (min, max, default) for the given sub-rig. + public void GetNormalizationAngle(int subRigIndex, out CubismPhysicsNormalization norm) + { + norm = default; + var sr = GetSubRig(subRigIndex); + if (!sr.TryGetProperty("Normalization", out var n)) return; + if (!n.TryGetProperty("Angle", out var angle)) return; + norm.Minimum = angle.TryGetProperty("Minimum", out var p) ? p.GetSingle() : -30f; + norm.Maximum = angle.TryGetProperty("Maximum", out var pp) ? pp.GetSingle() : 30f; + norm.Default = angle.TryGetProperty("Default", out var pd) ? pd.GetSingle() : 0f; + } + + /// Reads a single input entry: parameter ID, weight, type, reflect flag. + public void GetInput(int subRigIndex, int inputIndex, out string parameterId, out float weight, + out CubismPhysicsSource type, out bool reflect) + { + var inp = GetSubRig(subRigIndex).GetProperty("Input")[inputIndex]; + parameterId = inp.GetProperty("Source").GetProperty("Id").GetString(); + weight = inp.TryGetProperty("Weight", out var w) ? w.GetSingle() : 1f; + type = ParseSource(inp.TryGetProperty("Type", out var t) ? t.GetString() : "X"); + reflect = inp.TryGetProperty("Reflect", out var r) && r.GetBoolean(); + } + + /// Reads a single output entry: destination parameter ID, vertex index, scales, type, reflect. + public void GetOutput(int subRigIndex, int outputIndex, out string parameterId, out int vertexIndex, + out float scaleX, out float scaleY, out float weight, out CubismPhysicsSource type, out bool reflect) + { + var outp = GetSubRig(subRigIndex).GetProperty("Output")[outputIndex]; + parameterId = outp.GetProperty("Destination").GetProperty("Id").GetString(); + vertexIndex = outp.TryGetProperty("VertexIndex", out var v) ? v.GetInt32() : 0; + scaleX = scaleY = outp.TryGetProperty("Scale", out var s) ? s.GetSingle() : 1f; + weight = outp.TryGetProperty("Weight", out var w) ? w.GetSingle() : 1f; + type = ParseSource(outp.TryGetProperty("Type", out var t) ? t.GetString() : "X"); + reflect = outp.TryGetProperty("Reflect", out var r) && r.GetBoolean(); + } + + /// Reads a single particle/vertex: mobility, delay, acceleration, radius, position. + public void GetParticle(int subRigIndex, int vertexIndex, + out float mobility, out float delay, out float acceleration, out float radius, + out float posX, out float posY) + { + var verts = GetSubRig(subRigIndex).GetProperty("Vertices")[vertexIndex]; + mobility = verts.TryGetProperty("Mobility", out var m) ? m.GetSingle() : 1f; + delay = verts.TryGetProperty("Delay", out var d) ? d.GetSingle() : 0f; + acceleration = verts.TryGetProperty("Acceleration", out var a) ? a.GetSingle() : 1f; + radius = verts.TryGetProperty("Radius", out var r) ? r.GetSingle() : 1f; + var pos = verts.GetProperty("Position"); + posX = pos.TryGetProperty("X", out var px) ? px.GetSingle() : 0f; + posY = pos.TryGetProperty("Y", out var py) ? py.GetSingle() : 0f; + } + + private JsonElement GetSubRig(int index) => + _json.RootElement.GetProperty("PhysicsSettings")[index]; + + private static CubismPhysicsSource ParseSource(string v) => v switch + { + "Angle" => CubismPhysicsSource.Angle, + "Y" => CubismPhysicsSource.Y, + _ => CubismPhysicsSource.X + }; + } +} diff --git a/source_code/Cubism/Framework/CubismPose.cs b/source_code/Cubism/Framework/CubismPose.cs new file mode 100644 index 0000000..ed003bc --- /dev/null +++ b/source_code/Cubism/Framework/CubismPose.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Manages pose-based part visibility from a Cubism .pose3.json file. + /// Poses organize model parts into mutual-exclusion groups — only one + /// part in each group is visible at a time. This is commonly used for + /// eye states (open/closed/surprised) and other binary/multi-state + /// part switches. + /// + /// How it works: + /// + /// On model change (detected via ModelPtr comparison), all + /// part groups are reset: the first part in each group gets + /// opacity 1, others get opacity 0 + /// Each frame, the currently visible part in each group + /// fades toward opacity 1 while all others fade toward 0 + /// Linked parts (defined by the "Link" array in pose data) + /// copy their parent's opacity, cascading visibility changes + /// + /// + /// The fade uses a constant rate (1 / fadeTimeSeconds per second). + /// A piecewise-linear alpha calculation with a crossing threshold (Phi=0.5) + /// prevents parts from being partially visible during transitions. + /// + public class CubismPose + { + private struct PartData + { + public string PartId; + public int PartIndex; + public int ParameterIndex; + public List Link; + } + + private readonly List> _partGroups = new(); + private readonly float _fadeTimeSeconds; + private IntPtr _lastModel = IntPtr.Zero; + + /// + /// Parses a .pose3.json byte buffer. + /// Extracts the global fade time and all part groups with their + /// linked-part chains. + /// + public CubismPose(byte[] pose3JsonBytes) + { + if (pose3JsonBytes == null || pose3JsonBytes.Length == 0) return; + + var json = JsonDocument.Parse(pose3JsonBytes); + var root = json.RootElement; + _fadeTimeSeconds = root.TryGetProperty("FadeInTime", out var ft) ? ft.GetSingle() : 0.5f; + + if (!root.TryGetProperty("Groups", out var groups)) return; + + foreach (var group in groups.EnumerateArray()) + { + var partList = new List(); + foreach (var part in group.EnumerateArray()) + { + var pd = new PartData + { + PartId = part.GetProperty("Id").GetString(), + PartIndex = -1, + ParameterIndex = -1, + Link = new List() + }; + + if (part.TryGetProperty("Link", out var links)) + { + foreach (var link in links.EnumerateArray()) + pd.Link.Add(new PartData { PartId = link.GetString(), PartIndex = -1, ParameterIndex = -1 }); + } + + partList.Add(pd); + } + _partGroups.Add(partList); + } + } + + /// + /// Per-frame pose update. Detects model pointer changes to trigger + /// a full reset, then fades parts within each group and propagates + /// opacities to linked parts. + /// Called from after expressions. + /// + public unsafe void UpdateParameters(CubismNativeModel model, float deltaTimeSeconds) + { + if (model == null || model.ModelPtr == IntPtr.Zero) return; + + if (_lastModel != model.ModelPtr) + { + _lastModel = model.ModelPtr; + Reset(model); + } + + foreach (var group in _partGroups) + DoFade(model, group, deltaTimeSeconds); + + CopyPartOpacities(model); + } + + private unsafe void Reset(CubismNativeModel model) + { + var idsPtr = model.GetPartIds(); + var opacities = model.GetPartOpacities(); + int partCount = model.GetPartCount(); + + foreach (var group in _partGroups) + { + for (int i = 0; i < group.Count; i++) + { + var pd = group[i]; + pd.PartIndex = -1; + pd.ParameterIndex = -1; + + for (int pi = 0; pi < partCount; pi++) + { + if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == pd.PartId) + { + pd.PartIndex = pi; + opacities[pi] = i == 0 ? 1f : 0f; + break; + } + } + + group[i] = pd; + } + } + } + + private unsafe void DoFade(CubismNativeModel model, List group, float deltaTimeSeconds) + { + const float Phi = 0.5f; + const float BackOpacityThreshold = 0.15f; + + var idsPtr = model.GetPartIds(); + var opacities = model.GetPartOpacities(); + int partCount = model.GetPartCount(); + + float newOpacity = 1f; + int visibleIndex = 0; + + for (int i = 0; i < group.Count; i++) + { + var pd = group[i]; + if (pd.PartIndex < 0) continue; + + if (opacities[pd.PartIndex] > 0.001f) + { + visibleIndex = i; + break; + } + } + + for (int i = 0; i < group.Count; i++) + { + var pd = group[i]; + if (pd.PartIndex < 0) continue; + + if (i == visibleIndex) + { + var fadeRate = deltaTimeSeconds / _fadeTimeSeconds; + newOpacity = MathF.Min(opacities[pd.PartIndex] + fadeRate, 1f); + opacities[pd.PartIndex] = newOpacity; + } + else + { + float a1; + if (newOpacity < Phi) + a1 = newOpacity * (Phi - 1f) / Phi + 1f; + else + a1 = (1f - newOpacity) * Phi / (1f - Phi); + + var backOpacity = (1f - a1) * (1f - newOpacity); + if (backOpacity > BackOpacityThreshold) + a1 = 1f - BackOpacityThreshold / (1f - newOpacity); + + opacities[pd.PartIndex] = MathF.Min(opacities[pd.PartIndex], a1); + } + } + } + + private unsafe void CopyPartOpacities(CubismNativeModel model) + { + var idsPtr = model.GetPartIds(); + var opacities = model.GetPartOpacities(); + int partCount = model.GetPartCount(); + + foreach (var group in _partGroups) + { + for (int gi = 0; gi < group.Count; gi++) + { + var pd = group[gi]; + if (pd.PartIndex < 0 || pd.Link == null) continue; + + var parentOpacity = opacities[pd.PartIndex]; + for (int li = 0; li < pd.Link.Count; li++) + { + var link = pd.Link[li]; + for (int pi = 0; pi < partCount; pi++) + { + if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == link.PartId) + { + link.PartIndex = pi; + opacities[pi] = parentOpacity; + pd.Link[li] = link; + break; + } + } + } + } + } + } + } +} diff --git a/source_code/Cubism/Framework/CubismRenderer2D.cs b/source_code/Cubism/Framework/CubismRenderer2D.cs new file mode 100644 index 0000000..45d0d10 --- /dev/null +++ b/source_code/Cubism/Framework/CubismRenderer2D.cs @@ -0,0 +1,661 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using Cthangover.Core.Utils; +using Godot; +using Cthangover.Live2D.Cubism.Core; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Builds and updates 2D mesh instances for rendering a Cubism model in Godot. + /// + /// Handles the full rendering pipeline: + /// + /// BuildModel — Creates instances from + /// native vertex/index data, applies textures, resolves shaders based + /// on drawable blend flags and mask configuration + /// UpdateModel — Updates visibility, opacity, and vertex + /// positions each frame by reading CubismCore dynamic flags + /// Mask support — Creates SubViewport-based mask textures + /// for drawables that have mask counts. Masked drawables use + /// specialized shaders (2d_cubism_mask_*) that composite the mask + /// texture with the main texture + /// + /// + /// Shader resolution delegates to . + /// Shader names follow the pattern: 2d_cubism_[norm|mask]_[mix|add|mul][_inv]? + /// where "norm" = no mask, "mask" = masked, "mix/add/mul" = blend mode, + /// "_inv" = inverted mask. + /// + public class CubismRenderer2D + { + private MeshInstance2D[] _meshInstances; + private ShaderMaterial[] _materials; + private readonly Node2D _parentNode; + private CubismNativeModel _model; + + private struct MaskGroupData + { + public SubViewport Viewport; + public ViewportTexture Texture; + public int[] MaskDrawableIndices; + public MeshInstance2D[] MaskMeshes; + public ArrayMesh[] MaskArrayMeshes; + public int ViewportSize; + public float OffsetX; + public float OffsetY; + } + + private readonly List _maskGroups = new(); + private int[] _drawableMaskGroupIndex; + private Color[] _drawableMaskChannel; + + /// + /// Creates a renderer that attaches mesh instances as children of + /// the given parent node. + /// + public CubismRenderer2D(Node2D parentNode) + { + _parentNode = parentNode; + } + + /// + /// Builds all mesh instances for the given model. + /// Clears any previously built meshes. Creates mask SubViewports first, + /// then iterates drawables in render order, building ArrayMesh instances + /// with the appropriate shader and texture. + /// + public unsafe void BuildModel(CubismNativeModel model) + { + _model = model; + ClearMeshes(); + + var dc = model.DrawableCount; + _meshInstances = new MeshInstance2D[dc]; + _materials = new ShaderMaterial[dc]; + GameLogger.Log("LIVE2D", $"BuildModel ENTER: drawables={dc} textures={model.Textures?.Length ?? 0} ppu={model.PixelsPerUnit} canvas={model.CanvasWidth}x{model.CanvasHeight}"); + + var renderOrders = model.GetRenderOrders(); + GameLogger.Log("LIVE2D", $"BuildModel S1: renderOrders ptr ok"); + var constantFlags = model.GetConstantFlags(); + var texIndices = model.GetTextureIndices(); + var vertexCounts = model.GetVertexCounts(); + var vertexPositions = model.GetVertexPositions(); + var vertexUvs = model.GetVertexUvs(); + var indexCounts = model.GetIndexCounts(); + var indices = model.GetIndices(); + var opacities = model.GetOpacities(); + var maskCounts = model.GetMaskCounts(); + GameLogger.Log("LIVE2D", $"BuildModel S2: all native ptrs fetched"); + + // Log first 10 renderOrders to understand mapping + var roSample = new System.Text.StringBuilder("renderOrders: "); + for (int i = 0; i < Math.Min(dc, 10); i++) + roSample.Append($"[{i}]={renderOrders[i]} "); + GameLogger.Log("LIVE2D", roSample.ToString()); + + // Sanity: check renderOrders contain valid drawable indices + for (int i = 0; i < dc; i++) + { + var ro = renderOrders[i]; + if (ro < 0 || ro >= dc) + { + GameLogger.Log("LIVE2D", $"BuildModel BAD_RO: renderOrders[{i}]={ro} — OUT OF RANGE [0,{dc - 1}]"); + } + } + GameLogger.Log("LIVE2D", $"BuildModel S3: renderOrders validated"); + + float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue; + for (int di = 0; di < dc; di++) + { + var vc = vertexCounts[di]; + if (vc <= 0 || vc > 100000) { GameLogger.Log("LIVE2D", $"BuildModel BOUNDS_DIAG: di={di} vc={vc} SKIP"); continue; } + var vp = vertexPositions[di]; + if (di == 0) + GameLogger.Log("LIVE2D", $"BuildModel: drawable[0] vc={vc} vp0=({vp[0].X},{vp[0].Y})"); + for (int v = 0; v < vc; v++) + { + if (vp[v].X < minX) minX = vp[v].X; + if (vp[v].Y < minY) minY = vp[v].Y; + if (vp[v].X > maxX) maxX = vp[v].X; + if (vp[v].Y > maxY) maxY = vp[v].Y; + } + } + GameLogger.Log("LIVE2D", $"BuildModel: vertexBounds=({minX:F3},{minY:F3})-({maxX:F3},{maxY:F3}) ppu={model.PixelsPerUnit}"); + + for (int di = 0; di < dc; di++) + { + var vc = vertexCounts[di]; + if (vc <= 0 || vc > 100000) continue; + var uv = vertexUvs[di]; + float uMin = 2, uMax = -1, vMin = 2, vMax = -1; + for (int i = 0; i < vc; i++) + { + if (uv[i].X < uMin) uMin = uv[i].X; + if (uv[i].Y < vMin) vMin = uv[i].Y; + if (uv[i].X > uMax) uMax = uv[i].X; + if (uv[i].Y > vMax) vMax = uv[i].Y; + } + GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] uvBounds=({uMin:F3},{vMin:F3})-({uMax:F3},{vMax:F3})"); + } + + // Build mask data + BuildMaskGroups(model, maskCounts); + + SetupMaskViewports(model); + + // Second pass: create drawable meshes — iterate by drawable index (matching C++ original) + GameLogger.Log("LIVE2D", $"BuildModel S4: entering mesh loop, dc={dc}"); + for (int di = 0; di < dc; di++) + { + int vCount = vertexCounts[di]; + int iCount = indexCounts[di]; + if (vCount <= 0 || vCount > 100000 || iCount <= 0 || iCount > 500000) continue; + + var mesh = new ArrayMesh(); + var surfaceArray = new Godot.Collections.Array(); + surfaceArray.Resize((int)Mesh.ArrayType.Max); + + var verts = new Vector2[vCount]; + var uvs = new Vector2[vCount]; + var idxs = new int[iCount]; + + var srcVerts = vertexPositions[di]; + var srcUvs = vertexUvs[di]; + var srcIndices = indices[di]; + + for (int v = 0; v < vCount; v++) + { + verts[v] = new Vector2(srcVerts[v].X * _model.PixelsPerUnit, -srcVerts[v].Y * _model.PixelsPerUnit); + uvs[v] = new Vector2(srcUvs[v].X, 1f - srcUvs[v].Y); + } + + for (int t = 0; t < iCount; t++) + idxs[t] = srcIndices[t]; + + surfaceArray[(int)Mesh.ArrayType.Vertex] = verts; + surfaceArray[(int)Mesh.ArrayType.TexUV] = uvs; + surfaceArray[(int)Mesh.ArrayType.Index] = idxs; + + mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surfaceArray); + + var mi = new MeshInstance2D { Mesh = mesh, ZIndex = renderOrders[di], ZAsRelative = false }; + var texIndex = texIndices[di]; + var texture = texIndex >= 0 && texIndex < model.Textures.Length ? model.Textures[texIndex] : null; + var opacity = opacities[di]; + GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] opacity={opacity:F3}"); + + var shader = ResolveShaderForDrawable(di, constantFlags, maskCounts); + + ShaderMaterial mat = null; + if (shader != null) + { + mat = new ShaderMaterial { Shader = shader }; + mat.SetShaderParameter("color_base", new Color(1f, 1f, 1f, 1f)); + mat.SetShaderParameter("color_multiply", new Color(1f, 1f, 1f, 1f)); + mat.SetShaderParameter("color_screen", new Color(0f, 0f, 0f, 0f)); + mat.SetShaderParameter("channel", new Color(0f, 0f, 0f, 0f)); + mat.SetShaderParameter("mask_scale", 1f); + mat.SetShaderParameter("mesh_offset", Vector2.Zero); + + if (texture != null) + mat.SetShaderParameter("tex_main", texture); + + if (maskCounts[di] > 0) + { + var groupIdx = _drawableMaskGroupIndex[di]; + if (groupIdx >= 0 && groupIdx < _maskGroups.Count) + { + var group = _maskGroups[groupIdx]; + if (group.Texture != null) + { + mat.SetShaderParameter("tex_mask", group.Texture); + mat.SetShaderParameter("channel", _drawableMaskChannel[di]); + mat.SetShaderParameter("mask_scale", 1f); + mat.SetShaderParameter("mesh_offset", new Vector2(group.OffsetX, group.OffsetY)); + } + } + } + + mi.Material = mat; + } + + if (texture != null) + { + mat.SetShaderParameter("tex_main", texture); + GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] texture Ok"); + } + else + { + GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] texture MISSING (texIndex={texIndex})"); + } + + mi.Visible = true; + _parentNode.AddChild(mi); + + GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] materialSet={mi.Material != null} visible={mi.Visible} parent={mi.GetParent()?.Name}"); + _meshInstances[di] = mi; + _materials[di] = mat; + } + GameLogger.Log("LIVE2D", $"BuildModel DONE: meshes created"); + } + + /// + /// Returns the union AABB of all drawable meshes in local pixel space. + /// Valid after has completed. + /// + public Aabb GetContentBounds() + { + if (_meshInstances == null) return new Aabb(); + + Aabb aabb = default; + bool first = true; + foreach (var mi in _meshInstances) + { + if (mi?.Mesh == null) continue; + var meshAabb = ((ArrayMesh)mi.Mesh).GetAabb(); + if (first) { aabb = meshAabb; first = false; } + else aabb = aabb.Merge(meshAabb); + } + return aabb; + } + + private unsafe void BuildMaskGroups(CubismNativeModel model, int* maskCounts) + { + _maskGroups.Clear(); + _drawableMaskGroupIndex = new int[model.DrawableCount]; + _drawableMaskChannel = new Color[model.DrawableCount]; + for (int i = 0; i < model.DrawableCount; i++) + _drawableMaskGroupIndex[i] = -1; + + var masks = model.GetMasks(); + var dc = model.DrawableCount; + var maskSetToGroup = new Dictionary(); + + for (int di = 0; di < dc; di++) + { + var count = maskCounts[di]; + if (count == 0) continue; + + var maskIndices = masks[di]; + var sorted = new int[count]; + for (int mi = 0; mi < count; mi++) + sorted[mi] = maskIndices[mi]; + System.Array.Sort(sorted); + + var keyBuilder = new System.Text.StringBuilder(); + for (int mi = 0; mi < count; mi++) + { + if (mi > 0) keyBuilder.Append(','); + keyBuilder.Append(sorted[mi]); + } + var key = keyBuilder.ToString(); + + if (!maskSetToGroup.TryGetValue(key, out var groupIdx)) + { + groupIdx = _maskGroups.Count; + maskSetToGroup[key] = groupIdx; + + var uniqueMaskDrawables = new System.Collections.Generic.List(); + for (int mi = 0; mi < count; mi++) + if (!uniqueMaskDrawables.Contains(sorted[mi])) + uniqueMaskDrawables.Add(sorted[mi]); + + _maskGroups.Add(new MaskGroupData + { + MaskDrawableIndices = uniqueMaskDrawables.ToArray() + }); + } + + _drawableMaskGroupIndex[di] = groupIdx; + + var maskSet = _maskGroups[groupIdx].MaskDrawableIndices; + float cr = 0f, cg = 0f, cb = 0f, ca = 0f; + for (int mi = 0; mi < count; mi++) + { + int slot = System.Array.IndexOf(maskSet, sorted[mi]); + if (slot >= 0) + { + switch (slot) + { + case 0: cr = 1f; break; + case 1: cg = 1f; break; + case 2: cb = 1f; break; + case 3: ca = 1f; break; + } + } + } + _drawableMaskChannel[di] = new Color(cr, cg, cb, ca); + } + } + + private unsafe void SetupMaskViewports(CubismNativeModel model) + { + for (int gi = 0; gi < _maskGroups.Count; gi++) + { + var group = _maskGroups[gi]; + var maskIndices = group.MaskDrawableIndices; + var channelCount = maskIndices.Length; + + var ppu = model.PixelsPerUnit; + float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue; + for (int mi = 0; mi < channelCount; mi++) + { + var di = maskIndices[mi]; + var vCount = model.GetVertexCounts()[di]; + var vp = model.GetVertexPositions()[di]; + for (int v = 0; v < vCount; v++) + { + float wx = vp[v].X * ppu; + float wy = -vp[v].Y * ppu; + if (wx < minX) minX = wx; + if (wy < minY) minY = wy; + if (wx > maxX) maxX = wx; + if (wy > maxY) maxY = wy; + } + } + + float pad = 4f; + float vpSize = Math.Max(maxX - minX + pad * 2f, maxY - minY + pad * 2f); + vpSize = Math.Max(vpSize, 64f); + int vpSizeInt = (int)Mathf.Ceil(vpSize); + vpSizeInt = NextPowerOfTwo(vpSizeInt); + + var viewport = new SubViewport + { + Size = new Vector2I(vpSizeInt, vpSizeInt), + TransparentBg = true, + RenderTargetUpdateMode = SubViewport.UpdateMode.Always + }; + _parentNode.AddChild(viewport); + + group.Viewport = viewport; + group.Texture = viewport.GetTexture(); + group.ViewportSize = vpSizeInt; + group.OffsetX = minX - pad; + group.OffsetY = minY - pad; + group.MaskMeshes = new MeshInstance2D[channelCount]; + group.MaskArrayMeshes = new ArrayMesh[channelCount]; + + for (int mi = 0; mi < channelCount; mi++) + { + var di = maskIndices[mi]; + var mi2d = new MeshInstance2D(); + viewport.AddChild(mi2d); + group.MaskMeshes[mi] = mi2d; + group.MaskArrayMeshes[mi] = BuildMaskArrayMesh(di, model, mi2d, mi, group.OffsetX, group.OffsetY); + } + + _maskGroups[gi] = group; + } + } + + private unsafe ArrayMesh BuildMaskArrayMesh(int drawableIndex, CubismNativeModel model, MeshInstance2D mi2d, int channelSlot, float offsetX, float offsetY) + { + var vCount = model.GetVertexCounts()[drawableIndex]; + var iCount = model.GetIndexCounts()[drawableIndex]; + if (vCount == 0 || iCount == 0) return null; + + var mesh = new ArrayMesh(); + var surfaceArray = new Godot.Collections.Array(); + surfaceArray.Resize((int)Mesh.ArrayType.Max); + + var verts = new Vector2[vCount]; + var uvs = new Vector2[vCount]; + var idxs = new int[iCount]; + + var srcVerts = model.GetVertexPositions()[drawableIndex]; + var srcUvs = model.GetVertexUvs()[drawableIndex]; + var srcIndices = model.GetIndices()[drawableIndex]; + var ppu = model.PixelsPerUnit; + + for (int v = 0; v < vCount; v++) + { + verts[v] = new Vector2(srcVerts[v].X * ppu - offsetX, -srcVerts[v].Y * ppu - offsetY); + uvs[v] = new Vector2(srcUvs[v].X, 1f - srcUvs[v].Y); + } + + for (int t = 0; t < iCount; t++) + idxs[t] = srcIndices[t]; + + surfaceArray[(int)Mesh.ArrayType.Vertex] = verts; + surfaceArray[(int)Mesh.ArrayType.TexUV] = uvs; + surfaceArray[(int)Mesh.ArrayType.Index] = idxs; + + mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surfaceArray); + + var maskTexIndex = model.GetTextureIndices()[drawableIndex]; + var texture = maskTexIndex >= 0 && maskTexIndex < model.Textures.Length ? model.Textures[maskTexIndex] : null; + + var shader = ResolveShader("2d_cubism_mask"); + if (shader == null) return mesh; + + var mat = new ShaderMaterial { Shader = shader }; + mat.SetShaderParameter("channel", ChannelFromSlot(channelSlot)); + if (texture != null) + mat.SetShaderParameter("tex_main", texture); + + mi2d.Mesh = mesh; + mi2d.Material = mat; + return mesh; + } + + private static Color ChannelFromSlot(int slot) => slot switch + { + 0 => new Color(1f, 0f, 0f, 0f), + 1 => new Color(0f, 1f, 0f, 0f), + 2 => new Color(0f, 0f, 1f, 0f), + _ => new Color(0f, 0f, 0f, 1f) + }; + + /// + /// Per-frame render update. Resets dynamic flags, then iterates all + /// mesh instances in render order, applying visibility, opacity, and + /// vertex position changes based on CubismCore dynamic flags. + /// + public unsafe void UpdateModel() + { + if (_model == null || _meshInstances == null) return; + + var dynamicFlags = _model.GetDynamicFlags(); + var opacities = _model.GetOpacities(); + var renderOrders = _model.GetRenderOrders(); + var multiplyColors = _model.GetMultiplyColors(); + var screenColors = _model.GetScreenColors(); + var dc = _model.DrawableCount; + + for (int di = 0; di < dc; di++) + { + var mi = _meshInstances[di]; + if (mi == null) continue; + var mat = _materials[di]; + + var flags = dynamicFlags[di]; + + if ((flags & CubismCoreBindings.VisibilityDidChange) != 0) + mi.Visible = (flags & CubismCoreBindings.IsVisible) != 0; + + mi.ZIndex = renderOrders[di]; + + var mc = multiplyColors[di]; + mat.SetShaderParameter("color_multiply", new Color(mc.X, mc.Y, mc.Z, mc.W)); + + var sc = screenColors[di]; + mat.SetShaderParameter("color_screen", new Color(sc.X, sc.Y, sc.Z, sc.W)); + + if ((flags & CubismCoreBindings.OpacityDidChange) != 0) + mat.SetShaderParameter("color_base", new Color(1f, 1f, 1f, opacities[di])); + + if ((flags & CubismCoreBindings.VertexPositionsDidChange) != 0) + { + UpdateMeshVertices(di, mi.Mesh as ArrayMesh); + } + } + + for (int gi = 0; gi < _maskGroups.Count; gi++) + { + var group = _maskGroups[gi]; + if (group.MaskDrawableIndices == null) continue; + for (int mi = 0; mi < group.MaskDrawableIndices.Length; mi++) + { + var di = group.MaskDrawableIndices[mi]; + if (di < 0 || di >= dc) continue; + var mFlags = dynamicFlags[di]; + if ((mFlags & CubismCoreBindings.VertexPositionsDidChange) == 0) continue; + UpdateMaskMeshVertices(di, group.MaskArrayMeshes[mi], gi); + } + } + + _model.ResetDynamicFlags(); + } + + private unsafe void UpdateMeshVertices(int drawableIndex, ArrayMesh mesh) + { + if (mesh == null) return; + + var vCount = _model.GetVertexCounts()[drawableIndex]; + var srcVerts = _model.GetVertexPositions()[drawableIndex]; + var ppu = _model.PixelsPerUnit; + + int stride = 8; + byte[] data = new byte[vCount * stride]; + + float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue; + for (int v = 0; v < vCount; v++) + { + float x = srcVerts[v].X * ppu; + float y = -srcVerts[v].Y * ppu; + BinaryPrimitives.WriteSingleLittleEndian(data.AsSpan(v * stride), x); + BinaryPrimitives.WriteSingleLittleEndian(data.AsSpan(v * stride + 4), y); + + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + mesh.SurfaceUpdateVertexRegion(0, 0, data); + + mesh.CustomAabb = new Aabb( + new Vector3(minX, minY, 0), + new Vector3(maxX - minX, maxY - minY, 0)); + } + + private unsafe void UpdateMaskMeshVertices(int drawableIndex, ArrayMesh mesh, int groupIndex) + { + if (mesh == null || groupIndex < 0 || groupIndex >= _maskGroups.Count) return; + + var group = _maskGroups[groupIndex]; + float offsetX = group.OffsetX; + float offsetY = group.OffsetY; + var ppu = _model.PixelsPerUnit; + + var vCount = _model.GetVertexCounts()[drawableIndex]; + var srcVerts = _model.GetVertexPositions()[drawableIndex]; + + int stride = 8; + byte[] data = new byte[vCount * stride]; + + float vMinX = float.MaxValue, vMinY = float.MaxValue, vMaxX = float.MinValue, vMaxY = float.MinValue; + for (int v = 0; v < vCount; v++) + { + float x = srcVerts[v].X * ppu - offsetX; + float y = -srcVerts[v].Y * ppu - offsetY; + BinaryPrimitives.WriteSingleLittleEndian(data.AsSpan(v * stride), x); + BinaryPrimitives.WriteSingleLittleEndian(data.AsSpan(v * stride + 4), y); + + if (x < vMinX) vMinX = x; + if (y < vMinY) vMinY = y; + if (x > vMaxX) vMaxX = x; + if (y > vMaxY) vMaxY = y; + } + + mesh.SurfaceUpdateVertexRegion(0, 0, data); + + mesh.CustomAabb = new Aabb( + new Vector3(vMinX, vMinY, 0), + new Vector3(vMaxX - vMinX, vMaxY - vMinY, 0)); + } + + private void ClearMeshes() + { + if (_meshInstances != null) + { + for (int i = 0; i < _meshInstances.Length; i++) + { + var mi = _meshInstances[i]; + if (mi != null) mi.QueueFree(); + } + } + _meshInstances = null; + _materials = null; + _drawableMaskGroupIndex = null; + _drawableMaskChannel = null; + + foreach (var group in _maskGroups) + group.Viewport.QueueFree(); + _maskGroups.Clear(); + } + + private unsafe Shader ResolveShaderForDrawable(int drawableIndex, byte* constantFlags, int* maskCounts) + { + var flags = constantFlags[drawableIndex]; + var hasMask = maskCounts[drawableIndex] > 0; + var isInverted = (flags & CubismCoreBindings.IsInvertedMask) != 0; + var isAdditive = (flags & CubismCoreBindings.BlendAdditive) != 0; + var isMultiplicative = (flags & CubismCoreBindings.BlendMultiplicative) != 0; + + string name; + + if (!hasMask) + { + name = isAdditive ? "2d_cubism_norm_add" : + isMultiplicative ? "2d_cubism_norm_mul" : "2d_cubism_norm_mix"; + } + else if (!isInverted) + { + name = isAdditive ? "2d_cubism_mask_add" : + isMultiplicative ? "2d_cubism_mask_mul" : "2d_cubism_mask_mix"; + } + else + { + name = isAdditive ? "2d_cubism_mask_add_inv" : + isMultiplicative ? "2d_cubism_mask_mul_inv" : "2d_cubism_mask_mix_inv"; + } + + return ResolveShader(name); + } + + private static Shader ResolveShader(string name) + { + try + { + var shader = Cthangover.Core.Mods.ModManager.Instance?.ResolveShader(name); + if (shader != null) + { + GameLogger.Log("LIVE2D", $"ResolveShader: '{name}' OK"); + return shader; + } + } + catch (Exception ex) + { + GameLogger.Log("LIVE2D", $"ResolveShader: '{name}' threw {ex.Message}", LogLevel.Error); + } + GameLogger.Log("LIVE2D", $"ResolveShader: '{name}' FAILED", LogLevel.Error); + return null; + } + + private static int NextPowerOfTwo(int v) + { + if (v <= 0) return 1; + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + return v + 1; + } + } +} diff --git a/source_code/Cubism/Framework/CubismTargetPoint.cs b/source_code/Cubism/Framework/CubismTargetPoint.cs new file mode 100644 index 0000000..a2c7976 --- /dev/null +++ b/source_code/Cubism/Framework/CubismTargetPoint.cs @@ -0,0 +1,100 @@ +using System; + +namespace Cthangover.Live2D.Cubism.Framework +{ + /// + /// Smooth target-point tracking for face/eye movement parameters. + /// Drives the ParamAngleX/Y and ParamEyeBallX/Y model parameters in + /// . + /// + /// Uses a velocity-based smoothing approach: each update computes a + /// desired velocity toward the target, applies acceleration limits, + /// enforces a maximum safe approach speed to prevent overshoot near + /// the target, and integrates velocity into position. + /// + /// The tracking is frame-rate independent — delta time is scaled by + /// a factor of 30 (matching Cubism's internal 30fps reference). + /// Acceleration and max velocity are clamped per Cubism SDK conventions + /// (FaceParamMaxV = 40, FrameToMaxSpeed = 4.5). + /// + public class CubismTargetPoint + { + private const float FaceParamMaxV = 40f; + private const float FrameToMaxSpeed = 4.5f; + + private float _faceTargetX, _faceTargetY; + private float _faceX, _faceY; + private float _faceVX, _faceVY; + private float _lastTimeSeconds; + private float _userTimeSeconds; + + /// Current smoothed X position. + public float X => _faceX; + + /// Current smoothed Y position. + public float Y => _faceY; + + /// + /// Sets the target point immediately. The actual position + /// will smoothly approach this target over subsequent calls to . + /// + public void Set(float x, float y) + { + _faceTargetX = x; + _faceTargetY = y; + } + + /// + /// Advances the smoothing simulation by one frame. + /// Computes delta-time-weighted velocity toward target, applies + /// acceleration clamping and safe approach speed limiting, then + /// integrates position. + /// + /// Called each frame from + /// before injecting face-tracking parameters. + /// + public void Update(float deltaTimeSeconds) + { + _userTimeSeconds += deltaTimeSeconds; + var deltaTimeWeight = (_userTimeSeconds - _lastTimeSeconds) * 30f; + _lastTimeSeconds = _userTimeSeconds; + + if (deltaTimeWeight <= 0f) return; + + var maxV = FaceParamMaxV / 30f; + var maxA = deltaTimeWeight * maxV / FrameToMaxSpeed; + + var dx = _faceTargetX - _faceX; + var dy = _faceTargetY - _faceY; + var distance = MathF.Sqrt(dx * dx + dy * dy); + if (distance < 0.01f) return; + + var vx = maxV * dx / distance; + var vy = maxV * dy / distance; + + var ax = vx - _faceVX; + var ay = vy - _faceVY; + var aLen = MathF.Sqrt(ax * ax + ay * ay); + if (aLen > 0f) + { + var clampedA = MathF.Min(aLen, maxA); + ax = ax * clampedA / aLen; + ay = ay * clampedA / aLen; + } + + var maxSafeV = 0.5f * (MathF.Sqrt(maxA * maxA + 8f * maxA * distance) - maxA); + var vLen = MathF.Sqrt(_faceVX * _faceVX + _faceVY * _faceVY); + if (vLen > maxSafeV && vLen > 0f) + { + var scale = maxSafeV / vLen; + _faceVX *= scale; + _faceVY *= scale; + } + + _faceVX += ax; + _faceVY += ay; + _faceX += _faceVX; + _faceY += _faceVY; + } + } +} diff --git a/source_code/Cubism/Handlers/Live2DService.cs b/source_code/Cubism/Handlers/Live2DService.cs new file mode 100644 index 0000000..f4dc3de --- /dev/null +++ b/source_code/Cubism/Handlers/Live2DService.cs @@ -0,0 +1,85 @@ +using Godot; +using Cthangover.Live2D.Cubism.Framework; + +namespace Cthangover.Live2D.Cubism.Services +{ + /// + /// Configuration for . + /// When is set, defaults to + /// to disable automatic layout, giving manual + /// coordinates full control. Otherwise is used. + /// + public class Live2DModelConfig + { + public string ModelPath; + public string ModId; + public string NodeName; + public Vector2? Position; + public Vector2? Scale; + public float AnchorX = 0.5f; + public float AnchorY = 0.5f; + public FitMode ScaleFit = FitMode.FitAuto; + } + + /// + /// High-level factory for creating Live2D model nodes and attaching them + /// to the scene tree. Encapsulates the boilerplate of instantiating + /// , adding to a parent, and calling + /// . + /// + public static class Live2DService + { + /// + /// Creates a , adds it to , + /// and loads the model from the specified mod. + /// + /// Parent node to attach the model to. + /// Path to the .model3.json file, relative to the mod root. + /// Registered mod ID containing the model assets. + /// Godot node name for the model instance. + /// The created and loaded model node. + public static CubismModelNode CreateModel( + Node parent, + string modelPath, + string nodeName, + string modId = null) + { + return CreateModel(parent, new Live2DModelConfig + { + ModelPath = modelPath, + ModId = modId, + NodeName = nodeName + }); + } + + /// + /// Creates a using full configuration. + /// When is provided, + /// is forced to + /// — manual coordinates take priority over + /// the automatic . + /// + public static CubismModelNode CreateModel(Node parent, Live2DModelConfig config) + { + var fitMode = config.Position.HasValue ? FitMode.None : config.ScaleFit; + + var model = new CubismModelNode + { + Name = config.NodeName, + AnchorX = config.AnchorX, + AnchorY = config.AnchorY, + ScaleFit = fitMode + }; + + parent.AddChild(model); + + if (config.Position.HasValue) + model.Position = config.Position.Value; + if (config.Scale.HasValue) + model.Scale = config.Scale.Value; + + model.LoadModelFromMod(config.ModelPath, config.ModId); + return model; + } + } +} diff --git a/source_code/Live2D.csproj b/source_code/Live2D.csproj new file mode 100644 index 0000000..e699475 --- /dev/null +++ b/source_code/Live2D.csproj @@ -0,0 +1,14 @@ + + + net8.0 + Live2D + Live2D.Cubism + disable + 12 + true + + + + + + diff --git a/source_code/build.bat b/source_code/build.bat new file mode 100644 index 0000000..f41edf9 --- /dev/null +++ b/source_code/build.bat @@ -0,0 +1,37 @@ +@echo off +setlocal + +set "PROJECT_DIR=%~dp0" +set "OUTPUT_DIR=%PROJECT_DIR%bin\Debug\net8.0" +set "TARGET_DIR=%PROJECT_DIR%..\dll" +set "CSPROJ_PATH=%PROJECT_DIR%Live2D.csproj" + +echo Building %CSPROJ_PATH% ... +dotnet build "%CSPROJ_PATH%" +if %ERRORLEVEL% neq 0 ( + echo BUILD FAILED + goto :pause_and_exit +) + +echo Build succeeded. Copying files to %TARGET_DIR% ... + +set "ERRORS=0" +for %%f in (Live2D.dll Live2D.pdb Live2D.deps.json) do ( + if not exist "%OUTPUT_DIR%\%%f" ( + echo ERROR: Source file not found: %OUTPUT_DIR%\%%f + set "ERRORS=1" + ) else ( + copy /y "%OUTPUT_DIR%\%%f" "%TARGET_DIR%\%%f" >nul + echo Copied: %%f + ) +) + +if %ERRORS% neq 0 ( + echo COPY ERRORS DETECTED + goto :pause_and_exit +) + +echo Done. + +:pause_and_exit +pause diff --git a/src/AddLive2DModelAction.cs b/src/AddLive2DModelAction.cs new file mode 100644 index 0000000..d1e00e8 --- /dev/null +++ b/src/AddLive2DModelAction.cs @@ -0,0 +1,62 @@ +using Cthangover.Core.Actions; +using Cthangover.Core.Scenes; +using Cthangover.Core.Utils; +using Godot; +using Cthangover.Live2D.Cubism.Services; + +namespace Mods.Live2D +{ + public class AddLive2DModelAction : IScenarioAction + { + public string Name => "add_live2d_model"; + + public void Run(IActionContext ctx) + { + var modelPath = ctx.GetParam("model_path"); + var modId = ctx.GetParam("mod_id"); + var nodeName = ctx.GetParam("node_name") ?? "Live2DModel"; + + if (string.IsNullOrEmpty(modelPath)) + { + ctx.LogWarning("LIVE2D", "AddLive2DModelAction: missing 'model_path'"); + return; + } + + var viewBox = SceneContextNode.FindNode("ViewBox"); + if (viewBox == null) + { + ctx.LogWarning("LIVE2D", "AddLive2DModelAction: ViewBox not found"); + return; + } + + var existing = viewBox.GetNodeOrNull(nodeName); + if (existing != null) + { + ctx.Log("LIVE2D", $"AddLive2DModelAction: '{nodeName}' already exists, skipping"); + return; + } + + var config = new Live2DModelConfig + { + ModelPath = modelPath, + ModId = modId, + NodeName = nodeName + }; + + var xStr = ctx.GetParam("x"); + var yStr = ctx.GetParam("y"); + if (!string.IsNullOrEmpty(xStr) && !string.IsNullOrEmpty(yStr) && + float.TryParse(xStr, out var x) && float.TryParse(yStr, out var y)) + { + config.Position = new Vector2(x, y); + } + + var scaleStr = ctx.GetParam("scale"); + if (!string.IsNullOrEmpty(scaleStr) && float.TryParse(scaleStr, out var s)) + config.Scale = new Vector2(s, s); + + Live2DService.CreateModel(viewBox, config); + ctx.Log("LIVE2D", $"AddLive2DModelAction: loaded '{modelPath}' from mod '{modId}' as '{nodeName}'"); + } + } +}