From a9d4417fce0af0dcd70165558a9a4338972b2dfd Mon Sep 17 00:00:00 2001 From: iisaev Date: Fri, 16 Oct 2026 16:18:18 +0000 Subject: [PATCH] upd: sync local changes Co-Authored-By: Claude Opus 5.5 --- dll/Live2D.deps.json | 303 +++- dll/Live2D.dll | Bin 75776 -> 95744 bytes dll/Live2D.pdb | Bin 47428 -> 58344 bytes locale/en.properties | 2 + locale/ru.properties | 2 + manifest.json | 5 +- mod.png | Bin 2027 -> 3914 bytes native/Live2DCubismCore.lib | Bin 0 -> 15320 bytes native/libLive2DCubismCore.so | Bin 0 -> 124424 bytes source_code/Cubism/Core/CubismCoreBindings.cs | 13 + source_code/Cubism/Framework/ACubismMotion.cs | 183 ++- .../Framework/CubismExpressionMotion.cs | 86 +- .../Cubism/Framework/CubismModelMatrix.cs | 170 ++- .../Cubism/Framework/CubismModelNode.cs | 277 +++- source_code/Cubism/Framework/CubismMotion.cs | 931 ++++++++---- .../Cubism/Framework/CubismMotionInternal.cs | 16 + .../Cubism/Framework/CubismMotionJson.cs | 18 +- .../Framework/CubismMotionQueueEntry.cs | 184 ++- .../Cubism/Framework/CubismNativeModel.cs | 168 ++- source_code/Cubism/Framework/CubismPhysics.cs | 1274 +++++++++++++---- .../Cubism/Framework/CubismPhysicsJson.cs | 299 ++-- source_code/Cubism/Framework/CubismPose.cs | 374 +++-- .../Cubism/Framework/CubismRenderer2D.cs | 145 +- .../Cubism/Framework/CubismTargetPoint.cs | 125 +- source_code/Cubism/Handlers/Live2DService.cs | 12 +- source_code/Properties/AssemblyInfo.cs | 6 + src/AddLive2DModelAction.cs | 1 + 27 files changed, 3446 insertions(+), 1148 deletions(-) create mode 100644 locale/en.properties create mode 100644 locale/ru.properties create mode 100644 native/Live2DCubismCore.lib create mode 100644 native/libLive2DCubismCore.so create mode 100644 source_code/Properties/AssemblyInfo.cs diff --git a/dll/Live2D.deps.json b/dll/Live2D.deps.json index 7a4e42a..e0e3e16 100644 --- a/dll/Live2D.deps.json +++ b/dll/Live2D.deps.json @@ -33,117 +33,258 @@ } } }, - "Microsoft.CodeAnalysis.Common/4.11.0": { + "Humanizer.Core/2.2.0": { "runtime": { - "lib/net8.0/Microsoft.CodeAnalysis.dll": { - "assemblyVersion": "4.11.0.0", - "fileVersion": "4.1100.24.37604" + "lib/netstandard1.0/Humanizer.dll": { + "assemblyVersion": "2.2.0.0", + "fileVersion": "2.2.0.0" + } + } + }, + "Microsoft.Bcl.AsyncInterfaces/5.0.0": { + "runtime": { + "lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": { + "assemblyVersion": "5.0.0.0", + "fileVersion": "5.0.20.51904" + } + } + }, + "Microsoft.CodeAnalysis.Common/3.11.0": { + "runtime": { + "lib/netcoreapp3.1/Microsoft.CodeAnalysis.dll": { + "assemblyVersion": "3.11.0.0", + "fileVersion": "3.1100.21.40306" } }, "resources": { - "lib/net8.0/cs/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.resources.dll": { "locale": "cs" }, - "lib/net8.0/de/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.resources.dll": { "locale": "de" }, - "lib/net8.0/es/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.resources.dll": { "locale": "es" }, - "lib/net8.0/fr/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.resources.dll": { "locale": "fr" }, - "lib/net8.0/it/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.resources.dll": { "locale": "it" }, - "lib/net8.0/ja/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.resources.dll": { "locale": "ja" }, - "lib/net8.0/ko/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.resources.dll": { "locale": "ko" }, - "lib/net8.0/pl/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.resources.dll": { "locale": "pl" }, - "lib/net8.0/pt-BR/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.resources.dll": { "locale": "pt-BR" }, - "lib/net8.0/ru/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.resources.dll": { "locale": "ru" }, - "lib/net8.0/tr/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.resources.dll": { "locale": "tr" }, - "lib/net8.0/zh-Hans/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.resources.dll": { "locale": "zh-Hans" }, - "lib/net8.0/zh-Hant/Microsoft.CodeAnalysis.resources.dll": { + "lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.resources.dll": { "locale": "zh-Hant" } } }, - "Microsoft.CodeAnalysis.CSharp/4.11.0": { + "Microsoft.CodeAnalysis.CSharp/3.11.0": { "dependencies": { - "Microsoft.CodeAnalysis.Common": "4.11.0" + "Microsoft.CodeAnalysis.Common": "3.11.0" }, "runtime": { - "lib/net8.0/Microsoft.CodeAnalysis.CSharp.dll": { - "assemblyVersion": "4.11.0.0", - "fileVersion": "4.1100.24.37604" + "lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": { + "assemblyVersion": "3.11.0.0", + "fileVersion": "3.1100.21.40306" } }, "resources": { - "lib/net8.0/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "cs" }, - "lib/net8.0/de/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "de" }, - "lib/net8.0/es/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "es" }, - "lib/net8.0/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "fr" }, - "lib/net8.0/it/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "it" }, - "lib/net8.0/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "ja" }, - "lib/net8.0/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "ko" }, - "lib/net8.0/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "pl" }, - "lib/net8.0/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "pt-BR" }, - "lib/net8.0/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "ru" }, - "lib/net8.0/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "tr" }, - "lib/net8.0/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "zh-Hans" }, - "lib/net8.0/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": { "locale": "zh-Hant" } } }, + "Microsoft.CodeAnalysis.Workspaces.Common/3.11.0": { + "dependencies": { + "Humanizer.Core": "2.2.0", + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "3.11.0", + "System.Composition": "1.0.31", + "System.IO.Pipelines": "5.0.1" + }, + "runtime": { + "lib/netcoreapp3.1/Microsoft.CodeAnalysis.Workspaces.dll": { + "assemblyVersion": "3.11.0.0", + "fileVersion": "3.1100.21.40306" + } + }, + "resources": { + "lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "cs" + }, + "lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "de" + }, + "lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "es" + }, + "lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "fr" + }, + "lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "it" + }, + "lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "ja" + }, + "lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "ko" + }, + "lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "pl" + }, + "lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "pt-BR" + }, + "lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "ru" + }, + "lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "tr" + }, + "lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "zh-Hans" + }, + "lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.Workspaces.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "System.Composition/1.0.31": { + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Convention": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31", + "System.Composition.TypedParts": "1.0.31" + } + }, + "System.Composition.AttributedModel/1.0.31": { + "runtime": { + "lib/netstandard1.0/System.Composition.AttributedModel.dll": { + "assemblyVersion": "1.0.31.0", + "fileVersion": "4.6.24705.1" + } + } + }, + "System.Composition.Convention/1.0.31": { + "dependencies": { + "System.Composition.AttributedModel": "1.0.31" + }, + "runtime": { + "lib/netstandard1.0/System.Composition.Convention.dll": { + "assemblyVersion": "1.0.31.0", + "fileVersion": "4.6.24705.1" + } + } + }, + "System.Composition.Hosting/1.0.31": { + "dependencies": { + "System.Composition.Runtime": "1.0.31" + }, + "runtime": { + "lib/netstandard1.0/System.Composition.Hosting.dll": { + "assemblyVersion": "1.0.31.0", + "fileVersion": "4.6.24705.1" + } + } + }, + "System.Composition.Runtime/1.0.31": { + "runtime": { + "lib/netstandard1.0/System.Composition.Runtime.dll": { + "assemblyVersion": "1.0.31.0", + "fileVersion": "4.6.24705.1" + } + } + }, + "System.Composition.TypedParts/1.0.31": { + "dependencies": { + "System.Composition.AttributedModel": "1.0.31", + "System.Composition.Hosting": "1.0.31", + "System.Composition.Runtime": "1.0.31" + }, + "runtime": { + "lib/netstandard1.0/System.Composition.TypedParts.dll": { + "assemblyVersion": "1.0.31.0", + "fileVersion": "4.6.24705.1" + } + } + }, + "System.IO.Pipelines/5.0.1": { + "runtime": { + "lib/netcoreapp3.0/System.IO.Pipelines.dll": { + "assemblyVersion": "5.0.0.1", + "fileVersion": "5.0.120.57516" + } + } + }, "Cthangover/1.0.0": { "dependencies": { "GodotSharp": "4.6.3", "GodotSharpEditor": "4.6.3", - "Microsoft.CodeAnalysis.CSharp": "4.11.0" + "Microsoft.CodeAnalysis.CSharp": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "3.11.0" }, "runtime": { "Cthangover.dll": { - "assemblyVersion": "0.0.0.0", - "fileVersion": "0.0.0.0" + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" } } } @@ -169,19 +310,89 @@ "path": "godotsharpeditor/4.6.3", "hashPath": "godotsharpeditor.4.6.3.nupkg.sha512" }, - "Microsoft.CodeAnalysis.Common/4.11.0": { + "Humanizer.Core/2.2.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" + "sha512": "sha512-rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==", + "path": "humanizer.core/2.2.0", + "hashPath": "humanizer.core.2.2.0.nupkg.sha512" }, - "Microsoft.CodeAnalysis.CSharp/4.11.0": { + "Microsoft.Bcl.AsyncInterfaces/5.0.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" + "sha512": "sha512-W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==", + "path": "microsoft.bcl.asyncinterfaces/5.0.0", + "hashPath": "microsoft.bcl.asyncinterfaces.5.0.0.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.Common/3.11.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-FDKSkRRXnaEWMa2ONkLMo0ZAt/uiV1XIXyodwKIgP1AMIKA7JJKXx/OwFVsvkkUT4BeobLwokoxFw70fICahNg==", + "path": "microsoft.codeanalysis.common/3.11.0", + "hashPath": "microsoft.codeanalysis.common.3.11.0.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.CSharp/3.11.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-aDRRb7y/sXoJyDqFEQ3Il9jZxyUMHkShzZeCRjQf3SS84n2J0cTEi3TbwVZE9XJvAeMJhGfVVxwOdjYBg6ljmw==", + "path": "microsoft.codeanalysis.csharp/3.11.0", + "hashPath": "microsoft.codeanalysis.csharp.3.11.0.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.Workspaces.Common/3.11.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-YAbH4LCJfh8DhDGwYzSHqvnF06lKkVwblr8C+GwIYCv0i3Rzqjnbversat+i2n9k8twQ43yxVGTYK5p/mIOj4w==", + "path": "microsoft.codeanalysis.workspaces.common/3.11.0", + "hashPath": "microsoft.codeanalysis.workspaces.common.3.11.0.nupkg.sha512" + }, + "System.Composition/1.0.31": { + "type": "package", + "serviceable": true, + "sha512": "sha512-I+D26qpYdoklyAVUdqwUBrEIckMNjAYnuPJy/h9dsQItpQwVREkDFs4b4tkBza0kT2Yk48Lcfsv2QQ9hWsh9Iw==", + "path": "system.composition/1.0.31", + "hashPath": "system.composition.1.0.31.nupkg.sha512" + }, + "System.Composition.AttributedModel/1.0.31": { + "type": "package", + "serviceable": true, + "sha512": "sha512-NHWhkM3ZkspmA0XJEsKdtTt1ViDYuojgSND3yHhTzwxepiwqZf+BCWuvCbjUt4fe0NxxQhUDGJ5km6sLjo9qnQ==", + "path": "system.composition.attributedmodel/1.0.31", + "hashPath": "system.composition.attributedmodel.1.0.31.nupkg.sha512" + }, + "System.Composition.Convention/1.0.31": { + "type": "package", + "serviceable": true, + "sha512": "sha512-GLjh2Ju71k6C0qxMMtl4efHa68NmWeIUYh4fkUI8xbjQrEBvFmRwMDFcylT8/PR9SQbeeL48IkFxU/+gd0nYEQ==", + "path": "system.composition.convention/1.0.31", + "hashPath": "system.composition.convention.1.0.31.nupkg.sha512" + }, + "System.Composition.Hosting/1.0.31": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fN1bT4RX4vUqjbgoyuJFVUizAl2mYF5VAb+bVIxIYZSSc0BdnX+yGAxcavxJuDDCQ1K+/mdpgyEFc8e9ikjvrg==", + "path": "system.composition.hosting/1.0.31", + "hashPath": "system.composition.hosting.1.0.31.nupkg.sha512" + }, + "System.Composition.Runtime/1.0.31": { + "type": "package", + "serviceable": true, + "sha512": "sha512-0LEJN+2NVM89CE4SekDrrk5tHV5LeATltkp+9WNYrR+Huiyt0vaCqHbbHtVAjPyeLWIc8dOz/3kthRBj32wGQg==", + "path": "system.composition.runtime/1.0.31", + "hashPath": "system.composition.runtime.1.0.31.nupkg.sha512" + }, + "System.Composition.TypedParts/1.0.31": { + "type": "package", + "serviceable": true, + "sha512": "sha512-0Zae/FtzeFgDBBuILeIbC/T9HMYbW4olAmi8XqqAGosSOWvXfiQLfARZEhiGd0LVXaYgXr0NhxiU1LldRP1fpQ==", + "path": "system.composition.typedparts/1.0.31", + "hashPath": "system.composition.typedparts.1.0.31.nupkg.sha512" + }, + "System.IO.Pipelines/5.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==", + "path": "system.io.pipelines/5.0.1", + "hashPath": "system.io.pipelines.5.0.1.nupkg.sha512" }, "Cthangover/1.0.0": { "type": "project", diff --git a/dll/Live2D.dll b/dll/Live2D.dll index 40bc7e9ab4096e667be25ef53064ca832210dc22..4d16d257ee350a0096a843b43be97deff649367b 100644 GIT binary patch literal 95744 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 literal 75776 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 z zPWz5A(D%^d(!Q$=bnu*u+_Z1t98GzA_#(H?_d5f<*mF5#9~jqRuLElE^&2SId%4@_ zJH$ZkO;@^2z9}0tr4_x>ZSg&Bpr7}C)@}7|E$gtCd%xsP^KCs>Lw7p&xjnwC&m*)+ zy_J5=zmxCB0&P(*gn#4C_k}kKrAT*Y->?h`^_+QuPo1yhO`A31F7)X^;3j29o^S_! zU;l^>`*-JvyVy77!b&QCOz!F%(2#n)e#BkkTXUs^sg{n7&XDg7fi|g6b-m;c`L;X${|xIBHP2b}}xED!n}BzU$5T?h%D<3TTi7am~IoZUJ5>huN= zdbMFr`dklss^L07{Zj8&sPAV!m)oNr#ArTu6l+JNxKdK6O-=hXz1zo> zkME>=MFZVWLT;A2(4xd{gFm1?4L?II;KycLz+a4GBsd`lxmoIODN-I2CkEuea4J#3 z>=q;D+d{Jv{($-+{6Td9{I>cT{2^6ibzl=>cEAZM(MS*AqvW7kefw z{qYImQ0wQ(Qscx-=5X&IF;&+xdA-@pQd|WW@(~R`RLZiwJ5OXt*HSw4Kk;v$j>g;| zeC{ape>X4Pmj5i}s`WpdV_7zaS@!Tu`b((aiUw|r38+*(HIT7Jb)srkD7`cxHD@o%W%gi^B9;%TJ+0oqzXwTd3{=A@;L0Y83q zFx-He1uYFK9E<~RQ$%BLcwadmidJb!*|B3;Q}6FIG39?In9i=E)!MD%{l<<}%eZ6h zT`N3LGc7gHN^U>e!jxyjZz<|(1+!-QwlrUsvTNR$*N$_kZ6iCPyi7QWTg>GhxPCP# zmZ}?mZ8^1TDQ!P>>AoZuc%jJWK8JA|k&a*e3oSRGjzBF0)o)y;aG8`y*HoQ^>J~@d zEcFWb_p2vFe{Ps+F-MXA#dB)?Oy?s~zmr|o6jNYmt)D5kAsxTk5U{|{k7rrn=l`_# zHBfF<*SY5&Nu#S7jb^0r;4$_DnV4YyY>#aS4nMKx$5>&889alrsfnzS?syb5l8`j^ z48bd!lqR8zG|-YXG^F4bR>DitmIPKI4XbwRr0_~#!YVvM0}Wnj^GcdORzu$-eUH5F z+vi^W%-Bi#`qp}{YjtexyU#x7?6c24`|PvNxmQ;&qm$UtOIzfl)sChz*8|71h8D{l z(6C*pWlOD4OD&f4sh{BKYQK8F)JpWRR@+9_`Z_q+>Q6;`W{i47@)Y#5Qj;EqKF8Oj zVN2a4-p`Dh(fLhc!-ld`_p6^CvE>W3J+HubmC`jw#I4yu%VLssVo?o*G5 zREBa9Y|N(&|Epx48kw8@wi*5Bv7W*bWR(w!ZzR-;bjUCCW|==IoPQJ0uhxivXylr{ z|NjJ=d)JJQh|M(nM$URX=_5XYKG4_V1An=a5%p=j7aZrI_=UW6c!DvcZpZf{qkwIA zcJB86c69+@H}0fmh*JJ;}+_3so zbf+57uU#;#?o-!xTnl)Gy$AGHx(}!UE4C&LxVb%z@A#g(7I&o8-Jv@G-`oFtkn&3x z9#9GW%9byxq<#u_`_`$qF4?Ibm6M58!heIni`8ov{17Jz#mK(^&R-b9uKVaf1hB7z zJMg8=XXEL$_x2NhNpjOYanNf68>|iLWBv<4C+CEI4sC6a*0)RR+oi=TMYbzNw*C5t z{-U*Cp9{`u{k5~_tt-`+H+%??{Y~oXc~1a-Kz+hW>d*8OFI+;nAoK~~!{G1Nqu}4C zr_`y<2k@-rE8Q1^LK*G>p8=iIFWF;&iTI>GpgtP~rYAO*^{x7Wo<*IucCY=QPFm3| zAJI8z`!@hzz;li{>xuy3>a}qzp})N0Uex-t*nNPv^!%>A)mn-@rk6DD`Y&ld0dT9; z(f=6gUJ-ao--$7LM&GACbJjQXqV;R*zOC=kpY;7qXQg-d$g=|X;PLq{>-+SRt3P24 zSb;VDzWdbgUAV=!1kU}wWBOwoFZJCi_?4p7gq|Ll@g?-*_Pj3(%7?71Xz3CCzJV|M z1|WGGp0~ce|3ci)qn+f{g&6TS)t6TN*!QM->a3ptesSYZeUIwMCCYycBluI_F^u5b z{f}6KxO;I-Mr~Ml=Eb5Ov0mxs=wE9u`V)HJg>h?GeeJ9d_+OWE{Hm0^-G#KVJ7kpa z5Y1O*-0u>~T|&86O5UsP-h6YQDm}Va;Ozq6R5Po0LfY%qkA3$G<$ke~`-RU#!sj93 zbHCb!bCdhgqvr$nt7|sm2{_^Woc{FLe-n7pvi$!H_`pWh_MHA*Xw+cVaKYxlH0V|`KJ+{S~zZ@6Rz@Q(B5+D=&2P!;%xpvh;fgBSeR zXZd)Z_ncme{VMo>%>QBV|FcyeZF|yEft~6}s~E!5*&@|r(wDCYJSp%INb*l@Cq;(O ztCL%>J9EoWs?Up5pNF2;2TuzAm}uZx(ZE-Pk`$YHRb@-8UY53A7J0rewO)r#y49P2p9sAvbKy;~>esDd`+-mztv?dN-3`oo zTKi)m3tDYcAs7Bh>?@(D@3jlQ77FRFo%MCWUqqR$8^0ZTNwocv^-|A&4;|C%alUs< zUln^RbV}%_tj%kE?XLlEZzqRX`zh;xtXnJidF}V8`_9H|A3z@vxTF31uFSNAWBSS7 z!S-YNuJ*UU`SZO=;I;Oh@Q{NYlkG2A-{`rmeU0yfzFPaOR!{qfP?sLkFYP{}Z@%RD zcJ{hWv5zkUAGW{K?ziYG{1$C~mE^9n=$%$s)aojWmi&EpOurWUaeG>?+4KqPKKR3YE?{wUX7QY8d#rKnr`_%hjQQwylT!%S(K&_M6n$z!EI}u*5?+DRHO>Dd|oY2pl z_W{7q!d4?#kuK`A)vWqN*oHs&6ktg`2zXK`PYdN)p?pI<9FC}i)_(%bT7LpKYyD@G zJZ`b>NsD!#wpjNYLU~>&-xSKr*2Cej`Xl`k;A8r4Sw^$g^P083sUHOWo4)S=zU=!4 za8~{wfbRE4Ae)UZMg~;a-v`+3KO1nBe;r_-|80QlgdP|A2BB{h`o;b@a<}-;1>El6 z06tgx+2T$=TO9UN&fWfp0Z0As=saI#(9`qPE$Y7@IO2=_p7hs_koD7cOfY|1wSb88U3s3#h%v$ zhH%&X#hwcUCI#jM-YKx=C!Yre9v67Rh2;6F;HO+jiVBDfE)+=wU++SZMDT+Ge^uZq zo3&2a%(X(oAw+z=;OhlX2%Zo;EqGe+CBc^jKPLDw!A}T&Lhx4wKjlJlR_$yhE^w#7 z2L&D%_^LqFA!P*a6nIczP2ht9j|+TNpbATQfjb2r>mlV>4_iDT_zA&J34Tg&YZc2_ ztE9hzuNOQactY^B;Az2^1YZ*TnBd0*KOy)D!A}W(N^q-J`1cBb!Pg6(5IiAxTJW^s z$IoW@R|W1|OZ=d~n!pDI9vAqkz=Ls?c~IbSfv*Zw>q(Cb+$r#&z?#4Z1s)gpsz7xv z%kLCe6ZoLO;{sn5sNOC-1=`q?T;6$s+Jx^~c`Ymc&F~A)_bknt-Gw>wC=Ya zwvJnWZGF>vr+!HPP`CTe^adz~t=ny|R$8Nd^3z7F_4$-P?OAHI#{`vlKg#HTdj5y1}$e(Asqpk&_h5}-Qw z+km&NVwp*4>o26_A4|zsFZg@V-_y$$hgT7v=qJ23LO6Er_W)-vBDd=qB1Res^qGt;}mft)I<7z?YcpDb<-U%GfeFE>smAj=b;CQVS@E$;oy+RN0^8mGa zhw24>KA=_`@$DoFJ1V>t0pqm>_(gykaok$Kt1y-tzYu&5;Jej&!0X`AHQslA9!gFC zYDAUi1K$Iv)g(A+>=G^lyg^+8`d&bd_hf7az7J62yYU0SZvxc#?(#O^2LN$Wi0>`o z`xSs1-z9t}@PmL_rLo7e)HI;RcY>|}{vx2p(+XDsz680n`Wux1{ab(G)+Ctxn;0LU2xDT?-hn#*o_vh!YBH9JmdrRmi#?csrn09o8i9Frda8 zuWtYz0o1C?x)FFcAoOhQ2V7$q?zS@OXPrCL zQGDMDPd2FQt$o)2w7T^MozgzvE55t@zvcgo-wNyvd?9cm@Iu?S+P>5Fk8MlAVSBlgIXa{WUcc{}Nk>gkJk{gQr5(vRzhgJYJvDo!B=h-=Atn=qL1hrQDzE2iBARRsCHl_ZRxF<@${NQ{nfQ zdYhF0D}B=jmixMX_#M1Hr*D(%U+Zs*+%M=4i`*~je-M5z=_fX`+_&_PSM&Oc{x+`u z8c!mc|9*so-9)$}d{ZZre#|#to&H0dI4v*tW1Q2p(mon~o0KEH@wWkIdadQ|2zTK8 zayjj%@#VCShA-Me8a9tu4g8oV?P?3=<^bl?R?Mbt_&lmk>c`bf`cd^=e7>h&R405d zs;~L}8zQ_%)lF?Lst1CFmuh!3YkiRP7n}YNCe0ne?ka#1i< zbxQ(I7IM{GI=@fNl#6rw)M&a=ohVj7D;D;tYH^<$LO-3dX(&<1%oc%Hz|X@=#mYW~ zu>xC)Je4k^wS8)QW(KA9siJ$`>*R}>Ty@bjaZNdWSQ3Xk#PoN+^mo7Xcfa&^zhS%I z^w;D|@Aj|gFN#WkO`&>!L9X|Azt`XW(%=0p{oRlL?sxlZc$hx!Z|d)Uh5qhme_g>$ zfA_on-S75yKL*zH*EHew7hFw$_haojOjAvz)L4pw>2vxp3I7jxIl{CxoQG^LIMl{ z=L90dWv!frQrW3gAeE`$wpCYwp^hv%gZW(HmS%E^GmKh75$Xt+MAj9dLQ$QpO6AQp zSs59U>S*!MA*7O(UAaQ8GE1~@*eO?>$*NN-A$bT> zWpXxM20D~4R$NN5lE^~|&>{pVJA=+`IaJ!?RPgGA42db!SV>KQWg&~1KbNkuR4$9> z%gvm;`j%8`FrB#tZ(i7ybMp0!{VOw)*_E`B0v#}BzOyn0wLFZ7q zaHecmf3u5~T&A+Jwi)_KCsQnBPm?{4Va33mF`w0cUcT5l)-9)uxp_t|H2CpS8eTZ( zoJNQ#XQ4Wed9_md`V2Zvo*9kZPVUfbb!AQQ)Vp%0$(hJ4IQhziQ@#NU(8`KQtdk2X z$e61`vpE>InFunu#Qv_u%BbE`DHa+j_zSL@t^=&^XK_IUu*W%!#ThOs zS(wOU?IO)x4|BJHj!It69;YNF%xXRfMdwgHkt<`)B0@=H$v2M7UH|D_-~@{ESft&x zf5I4tOB*ikg0~T`XMzH=klawp!YQz6I1-F!A)S{5bSPI7Mu=#PFQ|z8;#5)aR0&R_ zP!+tu%%P;!x_IbODHwW4w$AWjF^#3vsxdKC+B7 zn1>U>%1~@7w$~|Fodqv)s5I&ugIjD6_U00BPGe}gvWAZSn{g)E$B@fJs6s^tnU z&R9dtNF@q~;Efs4z~3egBj}>fLNzabRLS971?#hrF)+paVF$h!{ejD5M@(tdp`;iHQaE3VJ+74x^UIffuPFY=C7+ z+Hh~An`J=gDHWj&vo48yZGJLxeNkfSPNEoWDPsh^N8xogXQ88@g*1pkP%cEB!%enaIQFyfU#I|Nfjw#A~r8T zR*Cs)kz7$Z-9X2om?PyJh(pD>5@upqB5O2}O;i#8P0u6i21b#&nH)XV5GHXW$4E!| za$1wbjjTka;>=BBjmTA-YnatW)N!O(zGY>3&RIBh2H7h!lLgEa#GuGb=U0?M05XsM zEn;z>%So8NlEYB3Fq1npUpD@)-t9fk%w&4TsV?rphP%?JW;7**_c;0Vf?!SgUN0xg z*s5o$jf${vm{23_6(qxSVX=|nP8y+C-36+VJGh83f!mS!0A#>JwtC1mgB;?5!i}9- zS_H#vGb9+d>m%!aFe{ZOMd z9KD#tRCy5sH!_-Ic4?%A;5xuMXL{AFc(*#KDTV1$c3FF~1>N?xlV)qI#3#x0Eafpn z>3pQ!n-bV5@tgx`DT4?`TvL5(D$d#syNt#S1wx_@H8zO=PnZdUenJy)VvZentNnmbD5yE_P#G%ZU*IK-$j4?zn; z6fcDxE#fckN4=35%<<@K(n*&yv*NShpiBcirW1AY=u4C!$lyF;be>(vshyDKSio%? zrxkKIHbJE9j-(_+EWt!1!8lsR&zlXQG}A&bY~9?{G(%k@RntyUYBi^!B-ujpD90cO zu?>A2;iPX-i3*N8hSK@`G~vL zc{8?HnaVgKG~??fOk9maOp|9eWTRNs$|&h=MNNHLmohdXD0W$mZBE|Zo5;|+7IK}; zZ9_>Fni8l<hg&W>CuLJvcMHHSa(hz%1DN}$a~Q~wlS2wV zW$W;!Y(CF1XBa(R=2~iT*@?Nr^LD0*AeYkjgb@Luj`ku)$(6@1zTe zgv#a&Z>qQ_oz0Qr9Kx=!Qcsr+`9?-1X5)d8M7)3ynZ*+o9?L6;u8@mUJ6z|>GuSbk zafBbLItzQSX)6}Eheq4jRJ-2EC5*ySI3CN$UZKfqvDAo06{4zgwJDv*WDrN0XhEf$ z6KIQJ*svQ)aM{g zkDVF!loVr6jZGEnI(LtE-5`0_e8JQ_gmJ_+OU~KFmg)&~phcc~1{{?+i*KgXaWY(f ziY1I!5QmH2?33+`~U-BxN#auiz*s$CLPLWY`aDHaSrM#!qOo1N9(uFiO zN%ire^5wwE%Sbs5n{dmzW8{^%2C=6cpo2@goJ|*siTt4=oY3qX4s1*i;vV(Mic%^f z>T|MQ?7JDGqc=5DE*Hyc zeENOx?d}m6ZfDX+n^F?BG^IE3mZw;btc(a`a1Wuv3-rfJYS!F*si?$sMGCrmY!j-A zHMUZW8eLD?>Q}K7cdNOEKAcPACQt?Y?TX5q8$`)$MR|b=@svxiD@ll6i599^X@*?D&|E5}E50&mRm zbf!))ci9>Qw$pmzFAXgtdD6h)d7LV)fK4QpI^F5Na6D}WYnT|JnQbdYI+eu94mKgK zV$A(qk3vaWDJgTK*DK()f?b_QVjVWqM3Q!z($`=x8MXO|s&xpxa-e`alS zj~X_K%fbngxtXvc+dCbq=cl-d@*Xg#!13{z3bw$hTxmoeCbm;q_ev#EP85-K#l=ak zZ_kjI!nU=H`$F!#@Dy0TUqr{!NN{*Lbs7|oZY!leJ)24m6-$e)rI_OJdPSA$SksB3 z8EtoXDq_|31Q)7$0tfFrqyZZo`fcNyZ<2Ei99q&`sG10_i)x}UR4|q6T`)eR`P{`* z6y$d^n~q#oWJ>%m4yazWq2+k&E*{&!Fl3)MligHWA|KUS3Zrm`a@ubjMdX-!bn;Qo07|_vyi(J z!)!-@U8ow?$Y>?N{WN9cGy_K^Fk^X?hW-F*$YKJ}cB-?>rKW{fXtoAP;k;7zsufj) zQ^Fn+_x9u_B-7(KX5lR-m(nzs#qZou#S=cV3B>V!MVY6MMivk>@PM>Fb>ZsdJ^eBZ13Y#CUST^hEQ=0Cku@lQljIZ$wfeEX{vc@HVuE=fB;bI0i#R?c_T014j zf}Q~Yr(Gpp5bZ!8W4Y2?8tK#wK>gT8ZoJTrpg|Ak?DTL6WZm@=k*%lrJrX;~%|egl zjWe%q1|(=o9WrjiSg4ub^%(*e=WbP+ND!(ppUyW?CK*b%rkhfY2w=HPlNs!R3ldC; zURu&-T6*HUhmebn!pkV>Y_^Qssf;k@OD4u}=cMb)qy?F7bwb75H*w2J915M|ETT{a zt(fDbWG>63!LSTreMCN#N$Yk~94{Abt%1eNS!h&gPPP^_R@NxllJ*phb_He2-tlM! z0VIxZ(E;z~2$pq2z?9&bsF&srdA_1}5|1sR`gZnU)QlEe&LI^=6k;@AT-f*Uxp71#U~|mc;H0DZh7QGindbVhFb~fiL8?2Wp`OrNjYVO zdT`SYx|Yk{G)^ZARcxR~xmV}Wf*~*n7%>lOO8a|?#j2O~&bHmz*y1#467V3fS;i#1 zu#Xrf!B;E|$xSkMRnohqXp&}?GXB|6u<^!uM|E*Grgn=#$YggZ#x4v6=3{WOQaK!VEI#g_4?k4_OdF*bu1$`+e`(Q++PM9iI`IMC0wKl|5bp;6m z$*8r~65Th~X(H6+K@h`n0SEDk^_1~|^<URVGp-{3$Q&#~5K8B6#*8y(6XtXa8w(uz z;O?C~TjbK<0rCz8!xb&)11WG|hpj4Yr#Ei|)*@=DYX`@DUV znCMWt#AhiN71p_tTM?7Gc(UMlsj;GY(hZL=yT~EBzH*L-oKPNCFrK|<>lE~bYd>gp zQc~PdJz4T9yN3zhmV@1LpEAeIJl?Y!FOSTX&(45*E~w+@J;1BzHO*_#-;tTI)eH+;iEc^0LucORjP9q^>X-j;w;k2hu1Fp zP%D~|HcYE2{QA>!3d#hlcuRQ+9Hw!t;$IPxst~1{A+HX$=ko0wMW6TLJ6c=u#tF4~ z6lJN2F|IrQ*Pu7w0$6R-cb+0qH8PGq}!6|E-Z!wF26u#;ao|hXwD2)G8Ketn z4PQ&36ssFnlmMOILUr&hz0=e8<9A6LJsz6`t%}x5(E1Gi;n5C!E!w!^mubU{aLg(gsvx>u`5P_iT(asC1Rv^dlVT?fwM ze?y*xs*C*TJKZuJlfaCxQBiL;(4!adE(8^%_hsAEJ#IF_a@a40-{qh#GN4vLQypfs zseRS6a(uj*Sj9JPTk3_zAb%B{ROgV`0mm2ZMQ;G49*2=9`O#s?XUfQ~LiCK{orAl; z6}5UBJm`VgEBNTQPXQ;7JmkltTcQ!qOSxsvX_TQ>*?Z%~-24m7cnzZdZOEl$6?|8i zvSJen3TGcJSanXJCEOoF3T;PQTjZG3*S^|4&8|J;I@=1wBV$ezD#MsV5&})215`OD zTk8~G6_4ZVmU9q1gRfZ9X&R-`yg9|$9Xd-6D<_$mSaeKf{G(wS;jK&Q68^RAs~TS2 z-&pIKJ3_H(Avw2kt{M$X`Nf@Lm(1G9`Rzy-`F?@8@Z;>JfAlP(RV`M+bmsqD26c_s zFK^|ZfV!GJ82wJO;#aQU;vdF1p!B}pEc`cl!!b{)W#e2U1H_}6md zc+hls6%G zf|;swnpZwES$0Y5uHWsV-%ZOz5+CKm>h8lT9=cI@Fsh_&lrRA>Q#;4y>ZYQ5r8jKN z<+k=Z;XMbAoJP0d>e-!&8c%F2oqm;GmU1%soyUjiG&J8x4}(9)%0#lNqY<~LPUDfw z(z5Ej5hd#}CcMWYB!}7PIoJ)#ONT#CDrxTkZ9PBhUVF+z{)+?|ZQYBkCgJ zE5;0-KPNZ38~FA$36!K3*cLTf?|0`Or0^9&^n#v^YlN&CI|e}^x8~?Kj!5M?ME)ai zZ?x2vksV^3UdAiUHY)L*L_QoDl!!8FYKi_vPT$(38jyC%;AeV`M;#zP`%mx9Xo|j@ z?hq^8*qOL&6)qfg6Iq!4(=~e>$cw8!&RwuV%PP=+rdHW=<@*)vYi?$5d`Klu-|`@0 zgkiivVh^Ndtut{1FT}~5f8%&p#S}`8!_UM~F9CgD1AWKweu^!aSC;Q)yd>oGtxaO# z_<60wn-&miKzjMk)AJe>Nv?zRlaPNHR+oSrX3TT!b%@+D#4xgxczaS3i<>$vH&d&1 z55V5sj9$4sup>E)vD^ZF8==vQmAdqFa*-29t?XJ@^FBvv26jOQDtH0jUI0&o_t|iK zU8}q3OdOY6F-BtzWt&G^D_S_Cjcph0nB4;<x4!?U$ z+ii}gNTqM4f5n41nA40f;Vt@`sq{%L`{y;yx!iNm)+mj_Z?uKmSD_xw9FtP-KHZ4NVa?-cc|^v`>ov!S zUWv8qbEBW9At_Mj#j`T3GJ23+pLe=kvokT}_-oYa7nb5U{-RR!&{*qE%d0V(bI`__ zT5K+L<`x^J+!il6T^}2zOp7VB%ivV~!qRN{mo051sFz;92P1=f_TWGdFex@|)&Y72 z?EXTGKgQvStSB*_8gVp_05_Cf@L@?cijg12`&f2A`a<}gwsQtQS^nmlfBLI8ena8+ zN-P`B+zJ4RMwyHVIw0_dU|d_#+A|S<(28B&9t=dc$1c}8c6nQGbi|4#QB&i$KVwU4 zB5kOOYbfrsVq-eG)bEeuWk^4MOE~u}eYaM=jFw`5Z z-J>I6Tt1FV1UhX^hk{W0N0Ei4O!9hEj}>%F8QRAom7$RZ#}j6vq50k*6kGd(j>Kw@ z;(vdDntxn+cPXoSd7(OHL$y7syT?N3BlxrN3aCgVx^xxv9IHJH4fNw>X<%;KutDtu zuzg5JY`Y6qS9>y6dkRfRGZ>krgl-GBZNcx(zfJkuVTfXcBM}> z_O*jqyA|#Zc6LBdx+~mgM_@%7uN~@OPad%%v0z6u$xK?&cKoY7PMb(}1_QCNSnY(4 zCZn~lf)pDQ)A(vQ7_?n8u(cfwFk$OeL8w~G=-XOTG5Ek*btDuFL?TG^2iPs2uOk@f zk5%nhRmZA=FW9jK9b4!K+OB{w#$Y?D#C998bVo3#Z7_&!(7LVLQvK5TSQQ$GEkuK% z*114i#2h_Jn?2eV)XarF+hWgvuWq?xa=QOrGC zhdZIdeYUNm`=DmMCfH?1_u=1^KwhWdx$t*Ru)9CpADt5ag$fb0(;v2@6S2|gM0CRR zB)SiMgrDgOhHN`J1*T-6Z4Spo8`?w1dYi_~__5kj^k^6Kdeo-+jWY`deX&^`n-$H( zW?jw1W!3nOT2)jZLAlyQ>YyX>aIWpbI^vv>+7>IO(K0zPgnS=;Ko?(ej9($ z+Ff>kwDu8RBjISRhDPqjSkm8_)V)|EjQ0cKBS>hGIt=%Q)uncqd%fGe-rEk(6I;3g z|Lt>wYs8I*8}pb0k4EpsR2FTv(Ixe$eZec-GF|Yf@bI;}@E90( z2yI2|rG7C<3XCRDBCO$Xg&QUhtB{8JTqP-hDDfi!SWImx6!e=J814!N4Tdox(v2DG zt%A71{ljB62(7u%qv2I6tPX{xk=nN4DsM@JqL=V7CTMhdoJknNaTiGT(GF(11zaT` zKP%`rbIql@mk##U#pA&addwz@n*lu>r$wy|u3~{Om316xbD>VPK0nxf=E|7sE(@qv zjNoML^TC_#F1ss$kKNUUX|3!3@D{@RL(;i>2YN8E|1bVX+8eMn#wIM050~Hqf9U*sJ3Kc5gX%x$$;T%nxVzH$qQ~ekw zdTi+!7Fl|BRO9-)wB~Fqs#TKVvhq^6oBkyhMqTo5O zl9TEgg)liZ^ejKfL?f^WL^;pmzqCxz&^p2gt^0y8V{Gub#^7A>BIpbP*vDZE=n^BG zK)ccmaUVyl8x$kz547QmY8+04P53X}z-TbmJIN83E-&NV!%#yyZyeM~ly!a8Ni35h z7#39Y5z7%EvDi|S%3caZg38Yz54#3fcWZm9^47=^dLZ-;O?Dq*5E#h z_qln;g(rvH-ww&CgQNUA*;+RMEJ#~^%eK0$xV6)d>MG1LMpHYhY7cF)YQMhhF@)wP zpEr5WCN!RguD?;?;&|(qAuf*Ni55Ofebwcgw(#G@@gY3phxbukRdD9<{=WRh@d><* zEtkQQB~y4uP~ocSE!$=;oq6ZXz(DrWE$MCPUTaEsPu`L%ffL@G!50P;3z*wjR@{wR zTXef_)Bd(t?J0TL*F%QR=8G$Wf%Trm@(l+vq{$PoR8sAHft-_{6$( zn=4gpmQuYE0LR)Xw)9COV3_0@!5y~g4T1z0x!XhTMK!LfLU&gf;q(K6uan<>Ug49z zwtibGyrJ6y9`!iNEOip}vd)qf!tbJ89vhQw4EDmJOW77pI5hpVJS2`u2d0s(m3brF+^*qrl8rVn>b;comx5EJf>?!$k) zcs^90TJYZDBEn&JQ_b+(h;Sd|!LTraizF6IVuAad?QED(V_OD1Ru#A)d$I_`0-c={ zV3UeH9AG$N<51KM@Zo*1&E%dV0u+Igpsc&D5AKLDGIpDRSei)8S%-GQv2-W$IWQ3c zmA;A{0?Og+LXWsGZ`01@@k>-;nYFAr=Nrq+tT;=?V~lt&*?-+C$tx5{Dcs7Fg1i zW08&^jD;1HsLoi)465$JsK5qI9D6S!8Vo1H7w&=?GTwoxSz8=eNQyN`qzAbx-iBm% z1UWGDZrvLXnY7Ka9yJ=bNi`IKT(E1kNGEy`D~WPy$Du=*V5!TF#9_#m9V>A)hK5=8 z4WlvAQHRT#-41mvg(PA^5ju6hLNjp1p*BS8T~-L`P+w5_`rL%o7s2rKaW#=om=*c} z`uZ?5*mDyG1Y(Z?oTd7}9J(|Y8|*A95=m{N30y*MOEgC-){W7MrDNU3c&u33w^4=h z(SePedIFz7+6G25CtUDvgnhRWt(`(%AJVZBkO*GSY{RoFh4&d-c(eIgHr3ar{bcv(zR@T$G;Kvh`ADhcmrnhm+a9b<51m<#KIh}}6XgA*DGwyP*}U~{&*bpr!$10LYVVp)yyxTNyA!*QJ5Q6x(7O)c+tvJP zZsousKGmD4zZ2`g9KOSNAkP;~WDl6PQsK+LS?2)0NpL{aaiHK-p{fJ!%P30OX*Ib! zvGvl+l-tyoZ*ZGJcc%LOX70cP|M;`QIp5Vk8oIjCm79sjTgCNK<4xt1N?|DRI<_%< zwMorYG8k7!-hd&ce|8?2Ebq3*=RdCh&ozMeze9#TrE0CBm|rQ~avD0z?Z#){P8bNo zO7G9N9oSx+u&3ln$UVT4a>pcv>vgykp2l-Yv& ziRRyS#JA7^p7VfLl^AzU?iCeqZ@m6|74n;-Vm5b~l=t}ae!97{#%Q~K2mLhlw!oQM zb-DJU9`9B+`Y`}Ww$wjthrggeO_;k~y!)STlB`*-O}G=p``G-0w@1BE@INFq7`>O! zC*Ch}pJ-Kal)GB^^UgWxyjw&5UcZ>)eX!R4H@9Q-W}ckMVzkYWcu{DlQxGip1iTjYM^<)HFui5;NsP?zK1fZB$<%cKM}Jsm!w|Q0^}i#T|3lROYa009q1Oha diff --git a/dll/Live2D.pdb b/dll/Live2D.pdb index fc6edba1bcd7ed84f6bbec2f9b29f453025930bf..802ae239edd4c632cf2f573171bef187b691d565 100644 GIT binary patch literal 58344 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 delta 26616 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"], - "depends_native": ["native/*.dll"], - "sources": ["src/*.cs"] + "depends": ["visual_novel_core>=1", "interface>=1"], + "depends_native": ["native/*.dll", "native/*.so"] } diff --git a/mod.png b/mod.png index d112f2e1b8139b0d6f27513f6e6b6f4819419956..be69cc8a9e5a4c57b77eedeb08c80a3999ee25c6 100644 GIT binary patch literal 3914 zcmb_f2{fD89uFb4MYVJxEjp&Pw3H%(AgWa~meyEG$C463EFl^yw%Q5Cw6E4SV`8+W zL0ZOMOoV!tC`zbWszmKeYboV@Y2VD5_s%=#%{lL#bMJTW_xt|8|8l>3|Nrkxwz0Yd z6_F7Efk04mvkO;2Aig|a7lHuD!EA#~U=R*8yA})riAwT1A1F8fC;&MC(C&!@fy4*RFBpFpIz0bo?8+*ziu+E&w)w}Zh}6gOg}1Xs)P^8rc0h1_N9ebcMtZ-Ur?M#tXrb0j zW7p&F9f$L}uwanVdn0>35C$C00Rc@O4oZUZ8V39o1AwpC@4&wb{lWTIp+8t(H2op; z*Ht4W!=rXXaH{ffRC^DlH*x9xq?W3QuK z`A`znrnjSlY}TBEpvH1QzuXs#e88?LSCN4IR6#l=0jPpaOA57qnR%B%j1(pEuklD% z(ej&20sa;U&IB!AtsMhm@C_fGY7w~YEGfj}(wf=A0$E>a0gx}FD4?j+QXYkm?tQE0 zTUGijfdlfR1R1^)7HG5I2lyl#0V41aUHi7(*Da9xbBKoYcvU&qG;A3e|+&<}sg^S&?}wcMUI49uV!G2?Zx$`e2+ zBafgAKBoy&KY$?fg5=&u+$Mrp^-( zu*&03$1&j7cr@i^Xy^MT0e%J_?YPspsUHSxs$bEXHrxWC+{C5vv>sjP8D_JxkTFf^ zNu21H;ul#0KunS__2B}vJaLO;#Lpl0-Swmy75f$$kA{LTPVHEh4)!YiOwvO+SIc<9>S? zYa~}VW#`hePXgw2!Z4a65A>d;&Fc?0N1QLBgt4?_>o~5hxOg%@c6MEkWfxGP(Sac z0vjG!-CIj)sx+zGWIMhjsO5I%y;P~3%O(60FpFx7jxdp~hA((sduC0g3W!nQZ4~6u zYA?7f)oB{OJNj_S1_x4rStOZY75Z}c7zF~hylXb%Y;evu!R~e-jEQj3IWWdVV=Tzy zl)a!lTua$%M{Ymb6Vj4!^{QLK0Dq76s24BY{gI^pLu%WJpi-Sn_*x(qwM43%1WZYJ zaUF@oz30nXlyGHn+4igJue|-j)y5 zoaRRc_-izkQn2aqMOFIJCIuQ#d1kk^pK6Ndq(JY=b|G~7T(L4K(4$>8l#cEXcZ1;- z2Dt?PqQ>V_gRVFVFPd*kAx2*Lx_A}14PI+<1l?FOFNTSIDaLBq$|*Q)ERp}LpdZ&- zlM|Wh-IaJgHWLU#>h(~djwJ<;yPBW4<*sFN*hsW29-;t~Ih1I&zPmFQI36gxs2|6% zK_E1088Pp5NPQRZ(LZ0m2`ozrLC10ca~o7%y5;E=2atXeBK5VrfGEvY_PBX~?;Z>o zFwmq+1NhzQ`A)_c4CAB;WV%WJT&-?*t?`2F0i+bL*h*7AM}%Xoj1k1n!$0`3@;*U&R&+)~gWDbZ`4J;*CN|KhnT|u$MEz-qn@d5C>YoZ`2&)cB? zr4XYuT^t49FsiDmsD{VR?tJ`=hu2l!1Wu4L*E`JN)rpenp?MpzzQnVt5p`_u8npiD zJJ1K*kWYb5GhuHEJ#+7cy4?A&QxtiwmhG-JQA0`4P%BmhL+dO+o{;<`LwBKp$25K<&jO^cYS|^L9YMa+YRzjCV8$Ae%OP*b0 z*1wYHGgf5$S3a(+NtBHlcX1~roCUn9+vnOPEzV~DqHy_O;sI=GhosTN$zrY)J0!l$ zploY+;xPN+qDICbIX|fr5rFVIeP(bjGDDHvweGv?y5Wm#Eqptt2sLbT@7mb(igWU{GMPM*1uYe6|nJ)beRYmbS+1IbJT%6 zJ0)(=CgdQ%^?UQlYVK2KnJXy?YE4%WzZ4cb_HLrU&P0;2S;0!!u)IZiEuQZ%({kcb zy~$OQ<4SK?2B)bQc89}!{y3#a4>nKUeK>1i=1`LEvxmWNA?nz(qeYkY{o<=shz(;AWeI}vucZXb+xR6b(mv-9xs0_#WMl>M|T z!6p-by#xCV=|i?#+netdLQr-@ZpIGb@O6XIi4+X8u5o$ej<&1<_6Tcc+GE0&OK|j! zzvjoxP2*0fJZo7MDqj4d(+-)z;{LEV)`1DFY}a2Cjq{95rcE|CYBr?%>~47^Ms>eU zyMKVmV&;v`E5ko1BcMq1d_kn^W?hypf5t+?K$?%eK~a?9qX$0QaH{5`;iL>-ex3c6 zbKxVpwTb)R1|c1nM*2#r;2JvTgzhR#30`hhUgUiv%#OjAp#yX2PqSHttQV@FeCzkq zRK%zxT`ke2J!yFP%QwlH9EDSMWg+$K4i=7;;H5^*mUCD**;bIDEw_jaK)Kq_69z4V z>7PbPAq9_yZC+7)x=E^9y6Uugt(r@F-zqR5H9rGY8=R=EkjfGL{xn?UV7qO`5$k^H zG>Kw;82+3Va{kH<$7d_|7cFqI^-(x2++u?|X-iv%BG~#y ztwMpc-IGL5|Df;u!w(-CG~<1sAtsOI7r*R6P!;vchL&||W8SRvkUMA(%&o0{*zX(Z zf)<|v1KR`_^)}*=Kw>p>y6pXS+nFaFvVQms z-`lB~P&~5saRklF5@+ZinzL72COv)4Cy#ADU6}AwgfNizqgYY z5c{$8U<{|{2c3A?4Qx4B(M_)Y<(f&s73R)8zOpQejn%#mMe*+z!gQn)?-+mdjIoG| zI1+VuPukOTT*u&2TOW{AtX_nVkVI*@=FBk#Q}Z%d=%Z3sPLKo3Yz2)2N_u$cOyP@G za@nw5tlZ&-!a->xBBKKIiF9oH|fuKDmYn;S{ZVtw`K z&J>{1x$Q&z*Ds$xSD^p%Pu;(%u9)597Xvx;U4cdix)Ol@V37Gms|%&)-ERK_2<4>W literal 2027 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 literal 0 HcmV?d00001 diff --git a/source_code/Cubism/Core/CubismCoreBindings.cs b/source_code/Cubism/Core/CubismCoreBindings.cs index 1deec03..35694e4 100644 --- a/source_code/Cubism/Core/CubismCoreBindings.cs +++ b/source_code/Cubism/Core/CubismCoreBindings.cs @@ -71,6 +71,19 @@ namespace Cthangover.Live2D.Cubism.Core [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] public static extern unsafe IntPtr csmReviveMocInPlace(void* address, uint size); + /// Native log callback: receives a pointer to a NUL-terminated csmChar* message. + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void CsmLogDelegate(IntPtr messagePtr); + + /// + /// Installs the Core log handler. Without it every native-side failure + /// (invalid moc3, unsupported version, alignment errors) is invisible. + /// Keep the delegate in a static field for the process lifetime — the + /// Core stores the raw function pointer. + /// + [DllImport(DllName, CallingConvention = CallingConvention.StdCall)] + public static extern void csmSetLogFunction(CsmLogDelegate logFunction); + /// 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); diff --git a/source_code/Cubism/Framework/ACubismMotion.cs b/source_code/Cubism/Framework/ACubismMotion.cs index b05da30..1794886 100644 --- a/source_code/Cubism/Framework/ACubismMotion.cs +++ b/source_code/Cubism/Framework/ACubismMotion.cs @@ -1,17 +1,21 @@ using System; +using System.Collections.Generic; 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. + /// Порт ACubismMotion из Cubism SDK for Native 5-r.5: владеет временами фейда, + /// весом, цикличностью и колбэками; очередь () + /// вызывает раз в кадр, тот в свою очередь — + /// (при старте), + /// и виртуальный . + /// + /// Завершение выставляется по EndTime: сам моушен ставит IsFinished в + /// DoUpdateParameters (незацикленный), а обёртка + /// после применения параметров закрывает запись, когда текущее время прошло EndTime + /// (например, после запущенного через очередь fade-out). Колбэки при этом не + /// дублируются: OnFinishedMotion вызывается только самим моушеном. /// public abstract class ACubismMotion { @@ -36,6 +40,9 @@ namespace Cthangover.Live2D.Cubism.Framework protected bool _isLoopFadeIn = true; protected bool _previousLoopState; + /// Сработавшие события (UserData); наполняется в . + protected readonly List _firedEventValues = new(); + /// /// Duration of the fade-in transition in seconds. /// A negative value means the default (1 second) will be used @@ -75,8 +82,8 @@ namespace Cthangover.Live2D.Cubism.Framework /// /// Invoked when the motion finishes (non-looping only). - /// Automatically called by - /// when timeOffset exceeds duration. + /// Автоматически вызывается самим моушеном в DoUpdateParameters по достижении + /// длительности; очередь этот колбэк не дублирует. /// public FinishedMotionCallback OnFinishedMotion { get; set; } @@ -101,73 +108,111 @@ namespace Cthangover.Live2D.Cubism.Framework public virtual float GetLoopDuration() => -1f; /// - /// Initializes a queue entry with start/end times based on current - /// , , - /// and . Fires . + /// Порт ACubismMotion::UpdateParameters — кадровая обёртка, вызываемая очередью. + /// Проверяет доступность записи, инициализирует её при старте, считает fade-вес, + /// применяет параметры и после этого закрывает запись, если время прошло EndTime. /// - public void SetupMotionQueueEntry(CubismMotionQueueEntry entry, float userTimeSeconds) + public void UpdateParameters(CubismNativeModel model, 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) + if (!entry.IsAvailable || entry.IsFinished) { - _previousLoopState = _isLoop; - UpdateEndTime(entry, duration); + return; + } + + SetupMotionQueueEntry(entry, userTimeSeconds); + + float fadeWeight = UpdateFadeWeight(entry, userTimeSeconds); + + DoUpdateParameters(model, userTimeSeconds, fadeWeight, entry); + + // Пост-проверка SDK: EndTime > 0 и уже пройден — запись завершена. + if ((entry.EndTime > 0f) && (entry.EndTime < userTimeSeconds)) + { + entry.IsFinished = true; } } + /// + /// Порт ACubismMotion::SetupMotionQueueEntry: при первом кадре записывает + /// StartTime (с учётом OffsetSeconds), FadeInStartTime и EndTime + /// (если EndTime ещё не выставлен извне, например фейдаутом от очереди). + /// + public void SetupMotionQueueEntry(CubismMotionQueueEntry entry, float userTimeSeconds) + { + if (!entry.IsAvailable || entry.IsFinished) + { + return; + } + + if (entry.IsStarted) + { + return; + } + + entry.IsStarted = true; + entry.StartTime = userTimeSeconds - _offsetSeconds; + entry.FadeInStartTime = userTimeSeconds; + + // EndTime мог быть выставлен заранее (SetFadeout до старта). + if (entry.EndTime < 0f) + { + AdjustEndTime(entry); + } + + OnBeganMotion?.Invoke(this); + } + + /// + /// Порт ACubismMotion::UpdateFadeWeight: вес = Weight x fadeIn x fadeOut + /// с синус-эйзингом. При fadeIn/fadeOut == 0 соответствующий множитель = 1. + /// + public float UpdateFadeWeight(CubismMotionQueueEntry entry, float userTimeSeconds) + { + if (entry == null) + { + return -1f; + } + + float fadeWeight = _weight; + + float fadeIn = _fadeInSeconds == 0f + ? 1f + : CubismMath.GetEasingSine((userTimeSeconds - entry.FadeInStartTime) / _fadeInSeconds); + + float fadeOut = (_fadeOutSeconds == 0f || entry.EndTime < 0f) + ? 1f + : CubismMath.GetEasingSine((entry.EndTime - userTimeSeconds) / _fadeOutSeconds); + + fadeWeight = fadeWeight * fadeIn * fadeOut; + + entry.SetState(userTimeSeconds, fadeWeight); + + return fadeWeight; + } + + /// + /// Порт ACubismMotion::AdjustEndTime: EndTime = StartTime + GetDuration(); + /// для цикличных моушенов (GetDuration() <= 0) EndTime = -1 (бесконечность). + /// + protected void AdjustEndTime(CubismMotionQueueEntry entry) + { + float duration = GetDuration(); + float endTime = duration <= 0f ? -1f : entry.StartTime + duration; + entry.EndTime = endTime; + } + + /// + /// Порт ACubismMotion::GetFiredEvent: базовая реализация не содержит событий. + /// переопределяет и наполняет список по UserData. + /// + public virtual List GetFiredEvent(float beforeCheckTimeSeconds, float motionTimeSeconds) + { + return _firedEventValues; + } + /// /// Applies motion parameter values to the model for the current frame. - /// Called every frame by . + /// Called every frame by . /// /// The native Cubism model to modify. /// Elapsed user time in seconds. diff --git a/source_code/Cubism/Framework/CubismExpressionMotion.cs b/source_code/Cubism/Framework/CubismExpressionMotion.cs index 16b4e05..da98dd4 100644 --- a/source_code/Cubism/Framework/CubismExpressionMotion.cs +++ b/source_code/Cubism/Framework/CubismExpressionMotion.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Text.Json; @@ -30,19 +29,27 @@ namespace Cthangover.Live2D.Cubism.Framework /// /// 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: + /// + /// Формулы применения — точный порт CubismExpressionMotion::DoUpdateParameters + /// через Set/Add/MultiplyParameterValue из CubismModel (SDK 5-r.5), weight — fade-вес: /// - /// — adds value * weight - /// — multiplies by 1 + value * weight - /// — lerps from current to target using weight + /// — value + expValue * weight + /// — value * (1 + (expValue - 1) * weight) + /// — value * (1 - weight) + expValue * weight /// - /// - /// Loaded and started by . + /// + /// Как и в SDK, результат зажимается в [min, max] параметра (порт + /// CubismModel::SetParameterValue), а параметры применяются в порядке их + /// перечисления в exp3.json: при нескольких записях с одним Id побеждает последняя. + /// Fade-время по умолчанию — 1 c (DefaultFadeTime), если в файле не задано. + /// + /// Загружается и запускается через . /// public class CubismExpressionMotion : ACubismMotion { + /// Порт DefaultFadeTime из CubismExpressionMotion.cpp. + private const float DefaultFadeTime = 1f; + private readonly List _parameters = new(); /// @@ -55,23 +62,23 @@ namespace Cthangover.Live2D.Cubism.Framework 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(); + // Порт CubismExpressionMotion::Parse: root[FadeInTime].ToFloat(DefaultFadeTime). + _fadeInSeconds = root.TryGetProperty("FadeInTime", out var fi) && fi.ValueKind == JsonValueKind.Number ? fi.GetSingle() : DefaultFadeTime; + _fadeOutSeconds = root.TryGetProperty("FadeOutTime", out var fo) && fo.ValueKind == JsonValueKind.Number ? fo.GetSingle() : DefaultFadeTime; if (root.TryGetProperty("Parameters", out var paramsArr)) { foreach (var p in paramsArr.EnumerateArray()) { - var blend = p.TryGetProperty("Blend", out var b) + // "Blend": null or absent -> Additive (SDK ToBlendFunction default). + var blend = p.TryGetProperty("Blend", out var b) && b.ValueKind == JsonValueKind.String ? ParseBlendType(b.GetString()) : ExpressionBlendType.Additive; _parameters.Add(new ExpressionParameter { ParameterId = p.GetProperty("Id").GetString(), - Value = p.GetProperty("Value").GetSingle(), + Value = p.TryGetProperty("Value", out var val) && val.ValueKind == JsonValueKind.Number ? val.GetSingle() : 0f, BlendType = blend }); } @@ -96,30 +103,41 @@ namespace Cthangover.Live2D.Cubism.Framework 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 indexById = new Dictionary(model.ParameterCount); + for (int i = 0; i < model.ParameterCount; i++) + indexById[CubismNativeModel.ReadStringFromPtrArray(idsPtr, i)] = i; + + // Порт: цикл по параметрам выражения (в порядке exp3.json). + foreach (var ep in _parameters) { - var pid = CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi); - foreach (var ep in _parameters) - { - if (ep.ParameterId != pid) continue; + if (!indexById.TryGetValue(ep.ParameterId, out int pi)) 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; + switch (ep.BlendType) + { + case ExpressionBlendType.Additive: + // AddParameterValue: value + (expValue * weight) + values[pi] = ClampParameterValue(values[pi] + ep.Value * weight, minValues[pi], maxValues[pi]); + break; + case ExpressionBlendType.Multiply: + // MultiplyParameterValue: value * (1 + (expValue - 1) * weight) + values[pi] = ClampParameterValue(values[pi] * (1f + (ep.Value - 1f) * weight), minValues[pi], maxValues[pi]); + break; + case ExpressionBlendType.Overwrite: + // SetParameterValue(id, value, weight): expValue зажимается, затем лерп + values[pi] = values[pi] * (1f - weight) + + ClampParameterValue(ep.Value, minValues[pi], maxValues[pi]) * weight; + break; } } } + + /// Порт GetParameterClampValue — зажим значения параметра в [min, max]. + private static float ClampParameterValue(float value, float min, float max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } } } diff --git a/source_code/Cubism/Framework/CubismModelMatrix.cs b/source_code/Cubism/Framework/CubismModelMatrix.cs index dd23988..e1f83e7 100644 --- a/source_code/Cubism/Framework/CubismModelMatrix.cs +++ b/source_code/Cubism/Framework/CubismModelMatrix.cs @@ -5,20 +5,29 @@ 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: + /// Port of CubismModelMatrix.cpp (Cubism SDK for Native 5-r.5). + /// Manages the 2D transformation (scale, translation, alignment) of a Cubism + /// model inside its logical coordinate space. + /// + /// SDK semantics to be aware of: /// - /// Cubism canvas coordinates (the model's native coordinate space) - /// Godot node local coordinates (where the model is placed) + /// and MULTIPLY the + /// current scale by target / canvas-size (they do not assign it), + /// exactly like the C++ original. + /// The (width, height) constructor performs + /// SetHeight(2.0f) like the SDK, i.e. the model is initially + /// scaled to occupy 2 logical units of height. + /// Translation methods are INCREMENTAL in the SDK: SetX/SetY, + /// SetPosition, Top, Left (via TranslateX/Y) add to the current + /// translation. SetupFromLayout is unaffected because it starts from a + /// fresh matrix (translation 0). + /// CenterX/CenterY/Bottom/Right compute their offset from the full + /// scaled size (canvas size × current scale). /// - /// - /// 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. + /// + /// reads positioning keys from a Cubism + /// .model3.json "Layout" section; writes the final + /// transform to a Godot . /// public class CubismModelMatrix { @@ -33,68 +42,80 @@ namespace Cthangover.Live2D.Cubism.Framework public CubismModelMatrix() { } /// - /// Creates a matrix referenced to the given canvas pixel dimensions. - /// Subsequent calls to and - /// will compute scale as target / canvas. + /// Creates a matrix referenced to the given canvas dimensions. + /// Like the SDK constructor, this calls (2.0f), + /// so the model initially occupies 2 logical units of height. /// public CubismModelMatrix(float width, float height) { _width = width; _height = height; + + SetHeight(2.0f); } - /// Current uniform scale on the X axis. + /// Current scale on the X axis. public float ScaleX => _scaleX; - /// Current uniform scale on the Y axis. + /// Current scale on the Y axis. public float ScaleY => _scaleY; - /// Current X translation offset in Godot local coordinates. + /// Current X translation offset. public float TranslateX => _translateX; - /// Current Y translation offset in Godot local coordinates. + /// Current Y translation offset. public float TranslateY => _translateY; + /// SDK CubismModelMatrix::Scale — multiplies the current scale. + public void Scale(float scaleX, float scaleY) + { + _scaleX *= scaleX; + _scaleY *= scaleY; + } + /// - /// Sets uniform scale to match the given width. Scale = targetWidth / canvasWidth. - /// Does nothing if canvas width is zero or unknown. + /// SDK SetWidth: multiplies the current uniform scale by w / canvasWidth. + /// Ignored when the canvas width is zero or unknown (the SDK would divide by zero). /// public void SetWidth(float w) { - if (_width > 0f) - { - var scale = w / _width; - _scaleX = scale; - _scaleY = scale; - } + if (_width <= 0f) return; + + var scaleX = w / _width; + var scaleY = scaleX; + Scale(scaleX, scaleY); } /// - /// Sets uniform scale to match the given height. Scale = targetHeight / canvasHeight. - /// Does nothing if canvas height is zero or unknown. + /// SDK SetHeight: multiplies the current uniform scale by h / canvasHeight. + /// Ignored when the canvas height is zero or unknown (the SDK would divide by zero). /// public void SetHeight(float h) { - if (_height > 0f) - { - var scale = h / _height; - _scaleX = scale; - _scaleY = scale; - } + if (_height <= 0f) return; + + var scaleX = h / _height; + var scaleY = scaleX; + Scale(scaleX, scaleY); } - /// Sets absolute translation position. - public void SetPosition(float x, float y) + /// SDK Translate — adds x and y to the current translation + /// (the SDK's TranslateX/TranslateY helpers are folded in here; the + /// TranslateX/TranslateY properties above are the read accessors). + public void Translate(float x, float y) { - _translateX = x; - _translateY = y; + _translateX += x; + _translateY += y; } - /// Sets absolute X translation. - public void SetX(float x) => _translateX = x; + /// SDK SetPosition — incremental (adds to the current translation). + public void SetPosition(float x, float y) => Translate(x, y); - /// Sets absolute Y translation. - public void SetY(float y) => _translateY = y; + /// SDK SetX — incremental (adds to the current X translation). + public void SetX(float x) => _translateX += x; + + /// SDK SetY — incremental (adds to the current Y translation). + public void SetY(float y) => _translateY += y; /// /// Centers the model horizontally and vertically at the given point. @@ -106,34 +127,45 @@ namespace Cthangover.Live2D.Cubism.Framework CenterY(y); } - /// - /// Centers horizontally: translateX = x - (width * scaleX) / 2. - /// - public void CenterX(float x) => _translateX = x - (_width * _scaleX) / 2f; + /// SDK CenterX — adds x - (width × scaleX) / 2 to the X translation. + public void CenterX(float x) + { + var w = _width * _scaleX; + _translateX += x - (w / 2f); + } + + /// SDK CenterY — adds y - (height × scaleY) / 2 to the Y translation. + public void CenterY(float y) + { + var h = _height * _scaleY; + _translateY += y - (h / 2f); + } + + /// SDK Top — adds y to the Y translation (via SetY). + public void Top(float y) => SetY(y); + + /// SDK Bottom — adds y - (height × scaleY) to the Y translation. + public void Bottom(float y) + { + var h = _height * _scaleY; + _translateY += y - h; + } + + /// SDK Left — adds x to the X translation (via SetX). + public void Left(float x) => SetX(x); + + /// SDK Right — adds x - (width × scaleX) to the X translation. + public void Right(float x) + { + var w = _width * _scaleX; + _translateX += x - w; + } /// - /// 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. + /// SDK SetupFromLayout — configures scale and position from a layout + /// dictionary (lowercased keys). Width/height are applied first (they + /// affect scale), then the position keys: "x", "y", "center_x", + /// "center_y", "top", "bottom", "left", "right". /// public void SetupFromLayout(Dictionary layout) { diff --git a/source_code/Cubism/Framework/CubismModelNode.cs b/source_code/Cubism/Framework/CubismModelNode.cs index 169dae0..98aab0b 100644 --- a/source_code/Cubism/Framework/CubismModelNode.cs +++ b/source_code/Cubism/Framework/CubismModelNode.cs @@ -2,6 +2,7 @@ using Cthangover.Core.Mods; using Cthangover.Core.UI.Event; using Cthangover.Core.Utils; using Godot; +using System.Collections.Generic; namespace Cthangover.Live2D.Cubism.Framework { @@ -36,6 +37,11 @@ namespace Cthangover.Live2D.Cubism.Framework private CubismRenderer2D _renderer; private CubismModelSettingJson _modelSetting; + // The autoload event controller this node registered its OnUpdate with. + // Kept so _ExitTree can unregister: the controller outlives the scene, + // and a stale registration would keep dispatching into a freed node. + private SceneEventController _eventController; + private CubismEyeBlink _eyeBlink; private CubismBreath _breath; private CubismPose _pose; @@ -53,7 +59,29 @@ namespace Cthangover.Live2D.Cubism.Framework private string _modelDir; private IModFileProvider _fileProvider; private bool _layoutLogged; - private readonly System.Collections.Generic.HashSet _manualParameters = new(); + // Parameters claimed by SetLiveParameter with their last written values. + // Re-applied after the Load/Save snapshot bracket every frame, so manual + // control survives the per-frame parameter restore. + private readonly System.Collections.Generic.Dictionary _manualValues = new(); + + // Cache of parameter index by id, built once after model load — hot loops + // (face-track injection, SetLiveParameter) resolve by index without + // per-frame Marshal.PtrToStringAnsi / string comparisons. + private readonly Dictionary _parameterIndexById = new(); + + private FaceTrackEntry[] _faceTrackCache = System.Array.Empty(); + + private readonly List _eyeBlinkIds = new(); + private readonly List _lipSyncIds = new(); + + /// One precomputed face-track injection: values[index] = targetAxis * scale. + private struct FaceTrackEntry + { + public int Index; + public string Id; + public bool FromTargetX; + public float Scale; + } /// /// All model parameters with their IDs, bounds, and current values. @@ -178,6 +206,13 @@ namespace Cthangover.Live2D.Cubism.Framework _nativeModel = new CubismNativeModel(); _nativeModel.LoadFromProvider(jsonBytes, _modelDir, _fileProvider); + if (!_nativeModel.IsLoaded) + { + // LoadFromProvider logs failures (missing moc, invalid JSON) and returns + // without a native model — FinishSetup would P/Invoke on IntPtr.Zero. + GameLogger.Log("LIVE2D", $"LoadModelFromMod: native model load failed for '{relativePath}'", LogLevel.Error); + return; + } FinishSetup(jsonBytes); } @@ -202,7 +237,10 @@ namespace Cthangover.Live2D.Cubism.Framework var ec = GetNodeOrNull("/root/EventController"); if (ec != null) + { + _eventController = ec; ec.AddUpdateEventListener(this); + } GameLogger.Log("LIVE2D", $"Model loaded: {_nativeModel.ParameterCount} params, {_nativeModel.DrawableCount} drawables, canvas={_nativeModel.CanvasWidth}x{_nativeModel.CanvasHeight}"); } @@ -211,15 +249,17 @@ namespace Cthangover.Live2D.Cubism.Framework /// /// Per-frame update called by the SceneEventController. - /// Execution order: + /// Execution order (SDK LAppModel::Update): /// /// Apply deferred layout (first valid frame only) /// Update for face tracking - /// Inject face-tracking parameters (ParamAngleX/Y, ParamEyeBallX/Y) - /// into the native model + /// Restore the parameter snapshot (LoadParameters) /// Evaluate motion queue (character animations) - /// Evaluate expression queue (facial expressions) - /// Run effect layers in order: breath, eye blink, pose, physics + /// Save the parameter snapshot (SaveParameters) — additive layers + /// (expressions, breath, face-track) start every frame from the same + /// baseline and never compound frame-over-frame + /// Run effect layers in SDK 5 scheduler order (ICubismUpdater.hpp): + /// eye blink → expression → look/face-track → breath → physics → pose /// Call csmUpdateModel on the native model /// Update the renderer (visibility, opacity, vertex positions) /// @@ -227,6 +267,10 @@ namespace Cthangover.Live2D.Cubism.Framework public void OnUpdate() { if (!_loaded) return; + // If the node was freed while detached from the tree, _ExitTree never + // ran and the stale registration survives: bail out before any Godot + // API call on the disposed meshes (ObjectDisposedException). + if (!GodotObject.IsInstanceValid(this) || !IsInsideTree()) return; var now = Time.GetTicksMsec(); var dt = _lastTickMsec == 0 ? 0.016f : (now - _lastTickMsec) / 1000f; @@ -237,45 +281,108 @@ namespace Cthangover.Live2D.Cubism.Framework _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; - } - } - } + // LAppModel.cpp:388-398 — Load/SaveParameters bracket around the motion + // phase. Without it, Additive/Multiply expressions and breath deltas + // accumulate frame-over-frame and drive parameters into their clamps. + _nativeModel.LoadParameters(); _motionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds); - _expressionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds); - _breath?.UpdateParameters(_nativeModel, dt); + _nativeModel.SaveParameters(); + + // Manual values survive the snapshot bracket: LoadParameters restores the + // pre-motion baseline and would otherwise silently revert SetLiveParameter. + ReapplyManualParameters(); + + // SDK 5 update-scheduler order (ICubismUpdater.hpp): + // EyeBlink(200) → Expression(300) → Look(400) → Breath(500) → Physics(600) → Pose(800). _eyeBlink?.UpdateParameters(_nativeModel, dt); - _pose?.UpdateParameters(_nativeModel, dt); + _expressionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds); + InjectFaceTrackParameters(); + _breath?.UpdateParameters(_nativeModel, dt); _physics?.Evaluate(_nativeModel, dt); + _pose?.UpdateParameters(_nativeModel, dt); _nativeModel.UpdateModel(); _renderer.UpdateModel(); } + /// + /// Frees the pinned native model memory when the node leaves the scene tree. + /// Without this the pinned GCHandles (moc + model buffers) leak for the rest + /// of the process — is unreachable otherwise. + /// + public override void _ExitTree() + { + // Invalidate before anything else: the autoload controller may + // dispatch OnUpdate between tree exit and the deferred delete pass, + // and the meshes die together with this subtree. + _loaded = false; + if (_eventController != null) + { + _eventController.RemoveUpdateEventListener(this); + _eventController = null; + } + _nativeModel?.Dispose(); + _nativeModel = null; + } + + /// + /// Adds the face-tracking target to the native parameter values via the + /// precomputed index cache — no string reads or comparisons in the per-frame + /// path. Parameters claimed through are skipped. + /// Mirrors SDK CubismLook::UpdateParameters (AddParameterValue): AngleX/Y = + /// target × 30, EyeBallX/Y = target — layered on top of the motion result, + /// never replacing it. The Load/Save bracket in + /// keeps the addition from accumulating across frames. + /// + private unsafe void InjectFaceTrackParameters() + { + if (_faceTrackCache.Length == 0) return; + + var values = _nativeModel.GetParameterValues(); + var tx = _targetPoint.X; + var ty = _targetPoint.Y; + + for (int i = 0; i < _faceTrackCache.Length; i++) + { + var entry = _faceTrackCache[i]; + if (_manualValues.ContainsKey(entry.Id)) continue; + values[entry.Index] += (entry.FromTargetX ? tx : ty) * entry.Scale; + } + } + + /// + /// Restores values set through after + /// LoadParameters resets the native parameter arrays to the snapshot. + /// + private unsafe void ReapplyManualParameters() + { + if (_manualValues.Count == 0) return; + + var values = _nativeModel.GetParameterValues(); + foreach (var kv in _manualValues) + { + if (!_parameterIndexById.TryGetValue(kv.Key, out var index)) continue; + values[index] = kv.Value; + } + } + private void DeferredApplyLayout() { if (_layoutLogged) return; if (!_contentBoundsReady) return; + // Layout from model3.json takes precedence over the Fit logic + // (same as LAppModel::SetupModel -> modelMatrix->SetupFromLayout). + var layout = _modelSetting?.GetLayoutMap(); + if (layout != null && layout.Count > 0) + { + ApplyLayoutFromModelSetting(layout); + return; + } + var parentSize = GetParentSize(); if (parentSize.X <= 0f || parentSize.Y <= 0f) return; @@ -300,6 +407,36 @@ namespace Cthangover.Live2D.Cubism.Framework GameLogger.Log("LIVE2D", $"ApplyLayout: parentSize={parentSize} contentSize={cw}x{ch} contentCenter={_contentCenter} scale={scale} pos={Position}"); } + /// + /// Applies the "Layout" section of model3.json through + /// (SDK semantics: + /// canvas logical units, model height defaults to 2 units via the matrix + /// constructor). The logical view scale maps 2 logical units to the + /// parent height (like the SDK samples' view matrix); Y is flipped + /// because Godot's Y axis points down. + /// + private void ApplyLayoutFromModelSetting(Dictionary layout) + { + var ppu = _nativeModel.PixelsPerUnit; + if (ppu <= 0f) ppu = 1f; + + var matrix = new CubismModelMatrix(CanvasWidth / ppu, CanvasHeight / ppu); + matrix.SetupFromLayout(layout); + + var parentSize = GetParentSize(); + var logicalPixelsPerUnit = parentSize.Y > 0f ? parentSize.Y / 2f : ppu; + + Scale = new Vector2( + matrix.ScaleX * logicalPixelsPerUnit / ppu, + matrix.ScaleY * logicalPixelsPerUnit / ppu); + Position = new Vector2( + matrix.TranslateX * logicalPixelsPerUnit, + -matrix.TranslateY * logicalPixelsPerUnit); + + _layoutLogged = true; + GameLogger.Log("LIVE2D", $"ApplyModelSettingLayout: matrixScale=({matrix.ScaleX:F3},{matrix.ScaleY:F3}) translate=({matrix.TranslateX:F3},{matrix.TranslateY:F3}) nodeScale={Scale} pos={Position}"); + } + private Vector2 GetParentSize() { var parent = GetParent(); @@ -329,6 +466,44 @@ namespace Cthangover.Live2D.Cubism.Framework Value = defaults[i] }; } + + // Parameter index cache is built once after load — hot loops + // resolve by index only. + _parameterIndexById.Clear(); + for (int i = 0; i < count; i++) + _parameterIndexById[Parameters[i].Id] = i; + + _faceTrackCache = BuildFaceTrackCache(); + } + + /// + /// Precomputes the face-track injection list once after load: + /// ParamAngleX/Y (×30) and ParamEyeBallX/Y (×1) — LAppModel semantics. + /// Parameters absent from the model are skipped. + /// + private FaceTrackEntry[] BuildFaceTrackCache() + { + var specs = new (string Id, bool FromTargetX, float Scale)[] + { + ("ParamAngleX", true, 30f), + ("ParamAngleY", false, 30f), + ("ParamEyeBallX", true, 1f), + ("ParamEyeBallY", false, 1f), + }; + + var result = new List(specs.Length); + foreach (var spec in specs) + { + if (!_parameterIndexById.TryGetValue(spec.Item1, out var index)) continue; + result.Add(new FaceTrackEntry + { + Index = index, + Id = spec.Item1, + FromTargetX = spec.Item2, + Scale = spec.Item3 + }); + } + return result.ToArray(); } private unsafe void SetupParts() @@ -351,13 +526,20 @@ namespace Cthangover.Live2D.Cubism.Framework private void SetupEffects() { - if (EnableEyeBlink && _modelSetting.GetEyeBlinkParameterCount() > 0) + // Collect the Groups (EyeBlink/LipSync) parameter ids once — they are + // wired into every motion via SetEffectIds (see StartMotion). + _eyeBlinkIds.Clear(); + for (int i = 0; i < _modelSetting.GetEyeBlinkParameterCount(); i++) + _eyeBlinkIds.Add(_modelSetting.GetEyeBlinkParameterId(i)); + + _lipSyncIds.Clear(); + for (int i = 0; i < _modelSetting.GetLipSyncParameterCount(); i++) + _lipSyncIds.Add(_modelSetting.GetLipSyncParameterId(i)); + + if (EnableEyeBlink && _eyeBlinkIds.Count > 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); + _eyeBlink.SetParameterIds(_eyeBlinkIds); } var physicsPath = _modelSetting.GetPhysicsFileName(); @@ -439,25 +621,18 @@ namespace Cthangover.Live2D.Cubism.Framework unsafe { - var values = _nativeModel.GetParameterValues(); - var idsPtr = _nativeModel.GetParameterIds(); - - for (int i = 0; i < _nativeModel.ParameterCount; i++) + if (!_parameterIndexById.TryGetValue(id, out var index)) { - var paramId = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i); - if (paramId != id) continue; - - values[i] = value; - _manualParameters.Add(id); - - if (i < Parameters.Length) - Parameters[i].Value = value; - + GameLogger.Log("LIVE2D", $"SetLiveParameter: parameter '{id}' not found", LogLevel.Warning); return; } - } - GameLogger.Log("LIVE2D", $"SetLiveParameter: parameter '{id}' not found", LogLevel.Warning); + _nativeModel.GetParameterValues()[index] = value; + _manualValues[id] = value; + + if (index < Parameters.Length) + Parameters[index].Value = value; + } } /// @@ -484,6 +659,10 @@ namespace Cthangover.Live2D.Cubism.Framework if (fadeIn >= 0f) motion.FadeInSeconds = fadeIn; if (fadeOut >= 0f) motion.FadeOutSeconds = fadeOut; + // Wire the motion's model curves (EyeBlink/LipSync) to the parameter + // ids from model3.json Groups — same as LAppModel.StartMotion. + motion.SetEffectIds(_eyeBlinkIds, _lipSyncIds); + _motionManager.StartMotion(motion, true, _userTimeSeconds); } diff --git a/source_code/Cubism/Framework/CubismMotion.cs b/source_code/Cubism/Framework/CubismMotion.cs index f04b8da..a733458 100644 --- a/source_code/Cubism/Framework/CubismMotion.cs +++ b/source_code/Cubism/Framework/CubismMotion.cs @@ -1,11 +1,22 @@ 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); + /// + /// Версия обработки зацикленного времени — порт CubismMotion::MotionBehavior (SDK 5-r.5). + /// V2 (по умолчанию): длительность цикла расширяется на один кадр (1/Fps), а переход + /// между циклами интерполируется через коррекцию конечной точки (CorrectEndPoint). + /// V1: время цикла жёстко перезаписывается без кадра-хвоста (поведение старых SDK). + /// + public enum MotionBehavior + { + V1 = 0, + V2 = 1, + } + /// /// Parses and plays a Cubism .motion3.json animation. /// Manages three categories of curves: @@ -14,22 +25,24 @@ namespace Cthangover.Live2D.Cubism.Framework /// 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 (): + /// + /// Данные хранятся как в нативном CubismMotionData: один общий плоский массив точек + /// () и общий массив сегментов (); + /// каждая кривая ссылается на свой диапазон сегментов через BaseSegmentIndex/SegmentCount. + /// Оценка кривой сначала ищет сегмент по времени ПЕРВОЙ точки следующего сегмента, + /// а за пределами последнего сегмента возвращает значение последней точки + /// (при зацикливании V2 — интерполирует переход конца в начало). + /// + /// Обновление параметров () — четыре прохода, порт SDK: /// - /// 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 + /// Model-кривые: EyeBlink/LipSync значения и ModelOpacity + /// Parameter-кривые: модуляция на EyeBlink (* значение) и LipSync (+ значение) + /// Не анимированные параметры групп EyeBlink/LipSync: лерп с fade-весом + /// PartOpacity-кривые /// - /// - /// Parameter fade-in/out is handled per-curve (overriding the motion-level - /// fade settings). Curve targets and IDs come from the motion3.json metadata. + /// + /// Фейд применяется по кривой: если у кривой заданы собственные FadeInTime/FadeOutTime — + /// используется они (база — фейд самого моушена), иначе общий fade-вес моушена. /// public class CubismMotion : ACubismMotion { @@ -37,27 +50,38 @@ namespace Cthangover.Live2D.Cubism.Framework private const string ModelCurveIdLipSync = "LipSync"; private const string ModelCurveIdOpacity = "Opacity"; + /// Порт SDK-константы MaxTargetSize: лимит целей EyeBlink/LipSync (ширина битовой маски). + private const int MaxTargetSize = 64; + + /// Порт CubismMath::Epsilon — порог сравнения в алгоритме Кардано. + private const float MathEpsilon = 0.00001f; + private readonly float _sourceFrameRate; private float _lastWeight; - private CubismMotionJson _motionJson; + private float _loopDurationSeconds = -1f; + private MotionBehavior _motionBehavior = MotionBehavior.V2; + private readonly CubismMotionJson _motionJson; + private readonly bool _areBeziersRestricted; private readonly List _modelCurves = new(); private readonly List _parameterCurves = new(); private readonly List _partOpacityCurves = new(); + // Общие плоские хранилища точек и сегментов (порт CubismMotionData.Points/Segments). + private CubismMotionPoint[] _points = Array.Empty(); + private MotionSegmentData[] _segments = Array.Empty(); + 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. + /// Parses a .motion3.json byte buffer: метаданные (длительность, loop, fps, фейды, + /// признак restricted-безье) и кривые/события через /. + /// Отсутствующее или отрицательное время фейда заменяется на 1 c — порт CubismMotion::Parse. /// public CubismMotion(byte[] motion3JsonBytes) { @@ -65,9 +89,12 @@ namespace Cthangover.Live2D.Cubism.Framework _sourceFrameRate = _motionJson.GetFps(); _areBeziersRestricted = _motionJson.AreBeziersRestricted(); _isLoop = _motionJson.IsLoop(); + _loopDurationSeconds = _motionJson.GetDuration(); - if (_motionJson.GetFadeInTime() >= 0f) _fadeInSeconds = _motionJson.GetFadeInTime(); - if (_motionJson.GetFadeOutTime() >= 0f) _fadeOutSeconds = _motionJson.GetFadeOutTime(); + var fadeIn = _motionJson.GetFadeInTime(); + var fadeOut = _motionJson.GetFadeOutTime(); + _fadeInSeconds = fadeIn < 0f ? 1f : fadeIn; + _fadeOutSeconds = fadeOut < 0f ? 1f : fadeOut; ParseCurves(); ParseEvents(); @@ -75,10 +102,9 @@ namespace Cthangover.Live2D.Cubism.Framework /// /// 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. + /// modulation (обычно из Groups модели в model3.json). Кривая параметра из + /// этих групп умножается на значение EyeBlink-кривой / складывается с LipSync + /// в проходе 2; параметры групп без собственной кривой получают лерп в проходе 3. /// public void SetEffectIds(List eyeBlinkIds, List lipSyncIds) { @@ -88,315 +114,706 @@ namespace Cthangover.Live2D.Cubism.Framework 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 -1 for looping motions (they never end), иначе полную длительность. + public override float GetDuration() => _isLoop ? -1f : _loopDurationSeconds; + + /// Полная длительность одного цикла, даже если моушен зациклен. + public override float GetLoopDuration() => _loopDurationSeconds; + + /// Порт CubismMotion::SetMotionBehavior — версия обработки зацикленного времени. + public void SetMotionBehavior(MotionBehavior behavior) => _motionBehavior = behavior; + + /// Порт CubismMotion::GetMotionBehavior. + public MotionBehavior GetMotionBehavior() => _motionBehavior; /// - /// Returns the full duration even when looping is active — used for computing - /// loop cycle length. + /// true, если среди Model-кривых есть кривая "Opacity" — порт CubismMotion::IsExistModelOpacity: + /// наличие кривой, а не текущее значение прозрачности. /// - 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) + public bool IsExistModelOpacity() { - _firedEventValues.Clear(); - foreach (var evt in _events) - if (evt.FireTime > beforeTime && evt.FireTime <= motionTime) - _firedEventValues.Add(evt.Value); - return 0f; + foreach (var curve in _modelCurves) + if (curve.Id == ModelCurveIdOpacity) return true; + return false; } + /// + /// Текущая ModelOpacity из Model-кривой "Opacity" (1, если кривой нет). + /// Рендерер должен умножать на неё прозрачность drawable'ов — SDK применяет + /// её в CubismModel::SetModelOpacity, здесь значение отдаётся наружу. + /// + public float GetModelOpacity() => _modelOpacity; + + /// + /// События (UserData), сработавшие в интервале (beforeCheckTime, motionTime]. + /// Список очищается при каждом вызове; очередь вызывает метод раз в кадр + /// с lastCheckTime..currentTime, поэтому события не теряются между кадрами. + /// + public override List GetFiredEvent(float beforeCheckTimeSeconds, float motionTimeSeconds) + { + _firedEventValues.Clear(); + foreach (var evt in _events) + if (evt.FireTime > beforeCheckTimeSeconds && evt.FireTime <= motionTimeSeconds) + _firedEventValues.Add(evt.Value); + return _firedEventValues; + } + + /// Совместимая обёртка над с тем же списком результатов. + public List GetFiredEventValue(float beforeTime, float motionTime) => + GetFiredEvent(beforeTime, motionTime); + /// /// Four-pass parameter update. Called by - /// every frame. + /// every frame (через ). /// public override unsafe void DoUpdateParameters( - CubismNativeModel model, float userTimeSeconds, float weight, CubismMotionQueueEntry entry) + CubismNativeModel model, float userTimeSeconds, float fadeWeight, 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) + // Порт CubismMotion::DoUpdateParameters: пересчёт EndTime при смене loop-состояния (V2). + if (_motionBehavior == MotionBehavior.V2 && _previousLoopState != _isLoop) { - while (timeOffset > duration) - timeOffset -= duration; + AdjustEndTime(entry); + _previousLoopState = _isLoop; } - var fadeWeight = weight; + float timeOffsetSeconds = userTimeSeconds - entry.StartTime; + if (timeOffsetSeconds < 0f) timeOffsetSeconds = 0f; // защита от старта в "будущем" + + float lipSyncValue = float.MaxValue; // FLT_MAX — сигнатура "кривой нет" + float eyeBlinkValue = float.MaxValue; + ulong lipSyncFlags = 0UL; + ulong eyeBlinkFlags = 0UL; + + // Базовые (моушен-уровень) фейды для кривых без собственных fade-настроек. + float tmpFadeIn = _fadeInSeconds <= 0f + ? 1f + : CubismMath.GetEasingSine((userTimeSeconds - entry.FadeInStartTime) / _fadeInSeconds); + + float tmpFadeOut = (_fadeOutSeconds <= 0f || entry.EndTime < 0f) + ? 1f + : CubismMath.GetEasingSine((entry.EndTime - userTimeSeconds) / _fadeOutSeconds); + + // 'Repeat' time as necessary. + float time = timeOffsetSeconds; + float duration = _loopDurationSeconds; + bool isCorrection = _motionBehavior == MotionBehavior.V2 && _isLoop; + + if (_isLoop) + { + if (_motionBehavior == MotionBehavior.V2) + duration += 1f / _sourceFrameRate; // кадр-хвост для интерполяции цикла + while (time > duration) + time -= duration; + } - 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]; + // Индексы параметров модели: порт model->GetParameterIndex(curveId). + var paramIndexById = new Dictionary(model.ParameterCount); + for (int i = 0; i < model.ParameterCount; i++) + paramIndexById[CubismNativeModel.ReadStringFromPtrArray(idsPtr, i)] = i; - // Pass 1 — model curves + // Pass 1 — model curves (EyeBlink / LipSync / Opacity). 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); - } + float value = EvaluateCurveInternal(curve, _points, _segments, time, isCorrection, duration, _areBeziersRestricted); - // Pass 2 — parameter curves - foreach (var curve in _parameterCurves) - { - var curveValue = EvaluateCurve(curve, timeOffset, fadeWeight); - - for (int pi = 0; pi < paramCount; pi++) + if (curve.Id == ModelCurveIdEyeBlink) eyeBlinkValue = value; + else if (curve.Id == ModelCurveIdLipSync) lipSyncValue = value; + else if (curve.Id == ModelCurveIdOpacity) { - 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; + // Порт: _modelOpacity = value; model->SetModelOpacity(...). + // Значение отдаётся наружу через GetModelOpacity(). + _modelOpacity = value; } } - // Pass 3 — remaining eye blink / lip sync (non-modulated params) - for (int pi = 0; pi < paramCount; pi++) + // Pass 2 — parameter curves с модуляцией EyeBlink/LipSync и пер-кривым фейдом. + foreach (var curve in _parameterCurves) { - 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; + if (!paramIndexById.TryGetValue(curve.Id, out int parameterIndex)) continue; + + float sourceValue = values[parameterIndex]; + float value = EvaluateCurveInternal(curve, _points, _segments, time, isCorrection, duration, _areBeziersRestricted); + + if (eyeBlinkValue != float.MaxValue) + { + for (int i = 0; i < _eyeBlinkParameterIds.Count && i < MaxTargetSize; i++) + { + if (_eyeBlinkParameterIds[i] == curve.Id) + { + value *= eyeBlinkValue; + eyeBlinkFlags |= 1UL << i; + break; + } + } + } + + if (lipSyncValue != float.MaxValue) + { + for (int i = 0; i < _lipSyncParameterIds.Count && i < MaxTargetSize; i++) + { + if (_lipSyncParameterIds[i] == curve.Id) + { + value += lipSyncValue; + lipSyncFlags |= 1UL << i; + break; + } + } + } + + float v; + if (curve.FadeInTime < 0f && curve.FadeOutTime < 0f) + { + // Собственных фейдов у кривой нет — применяется общий fade-вес моушена. + v = sourceValue + (value - sourceValue) * fadeWeight; + } + else + { + // Порт: база — фейд самого моушена (tmpFadeIn/tmpFadeOut), не 1. + float fin = curve.FadeInTime < 0f + ? tmpFadeIn + : curve.FadeInTime == 0f + ? 1f + : CubismMath.GetEasingSine((userTimeSeconds - entry.FadeInStartTime) / curve.FadeInTime); + + float fout = curve.FadeOutTime < 0f + ? tmpFadeOut + : (curve.FadeOutTime == 0f || entry.EndTime < 0f) + ? 1f + : CubismMath.GetEasingSine((entry.EndTime - userTimeSeconds) / curve.FadeOutTime); + + float paramWeight = _weight * fin * fout; + v = sourceValue + (value - sourceValue) * paramWeight; + } + + // Порт model->SetParameterValue: значение зажимается в [min, max] параметра. + values[parameterIndex] = ClampParameterValue(v, minValues[parameterIndex], maxValues[parameterIndex]); } - // Pass 4 — part opacity curves + // Pass 3 — EyeBlink/LipSync для параметров, не анимированных кривыми: лерп с fade-весом. + if (eyeBlinkValue != float.MaxValue) + { + for (int i = 0; i < _eyeBlinkParameterIds.Count && i < MaxTargetSize; i++) + { + // Моушен уже записал этот параметр в проходе 2 — моргание не применяем. + if ((eyeBlinkFlags >> i & 1UL) != 0UL) continue; + if (!paramIndexById.TryGetValue(_eyeBlinkParameterIds[i], out int pi)) continue; + + float sourceValue = values[pi]; + float v = sourceValue + (eyeBlinkValue - sourceValue) * fadeWeight; + values[pi] = ClampParameterValue(v, minValues[pi], maxValues[pi]); + } + } + + if (lipSyncValue != float.MaxValue) + { + for (int i = 0; i < _lipSyncParameterIds.Count && i < MaxTargetSize; i++) + { + if ((lipSyncFlags >> i & 1UL) != 0UL) continue; + if (!paramIndexById.TryGetValue(_lipSyncParameterIds[i], out int pi)) continue; + + float sourceValue = values[pi]; + float v = sourceValue + (lipSyncValue - sourceValue) * fadeWeight; + values[pi] = ClampParameterValue(v, minValues[pi], maxValues[pi]); + } + } + + // Pass 4 — part opacity curves (пишутся напрямую в массив прозрачностей частей). var partOpacities = model.GetPartOpacities(); var partIdsPtr = model.GetPartIds(); int partCount = model.GetPartCount(); + var partIndexById = new Dictionary(partCount); + for (int i = 0; i < partCount; i++) + partIndexById[CubismNativeModel.ReadStringFromPtrArray(partIdsPtr, i)] = i; + foreach (var curve in _partOpacityCurves) { - var val = EvaluateCurve(curve, timeOffset, fadeWeight); - for (int pi = 0; pi < partCount; pi++) + if (!partIndexById.TryGetValue(curve.Id, out int pi)) continue; + partOpacities[pi] = EvaluateCurveInternal(curve, _points, _segments, time, isCorrection, duration, _areBeziersRestricted); + } + + // Завершение/перезапуск: моушен сам выставляет IsFinished и зовёт колбэк + // (очередь только удаляет finished-записи — порт CubismMotionQueueManager). + if (timeOffsetSeconds >= duration) + { + if (_isLoop) + UpdateForNextLoop(entry, userTimeSeconds, time); + else { - var pid = CubismNativeModel.ReadStringFromPtrArray(partIdsPtr, pi); - if (pid == curve.Id) - { - partOpacities[pi] = val; - break; - } + OnFinishedMotion?.Invoke(this); + entry.IsFinished = true; } } - // 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; - } + _lastWeight = fadeWeight; } - private float EvaluateCurve(MotionCurveData curve, float time, float weight) + /// + /// Порт CubismMotion::UpdateForNextLoop: сдвиг StartTime назад на время внутри цикла; + /// фейд-ин перезапускается только при включённом _isLoopFadeIn (поведение V2). + /// + private void UpdateForNextLoop(CubismMotionQueueEntry entry, float userTimeSeconds, float time) { - 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++) + switch (_motionBehavior) { - 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; - } - + case MotionBehavior.V2: default: - return CubismMath.LinearEvaluation(ref p0, ref p1, time); + entry.StartTime = userTimeSeconds - time; + if (_isLoopFadeIn) + entry.FadeInStartTime = userTimeSeconds - time; + OnFinishedMotion?.Invoke(this); + break; + case MotionBehavior.V1: + entry.StartTime = userTimeSeconds; + if (_isLoopFadeIn) + entry.FadeInStartTime = userTimeSeconds; + break; } } - 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]; - } - + /// + /// Разбор кривых: порт CubismMotion::Parse. Все точки и сегменты складываются + /// в общие плоские списки; кривая хранит BaseSegmentIndex и количество сегментов. + /// private void ParseCurves() { - var segsFlat = new float[_motionJson.GetTotalSegmentCount() * 2]; - var segTypesFlat = new CubismMotionSegmentType[_motionJson.GetTotalSegmentCount()]; - var allPoints = new List(); - int segOffset = 0; + var points = new List(); + var segments = new List(); 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 target = ParseTarget(_motionJson.GetCurveTarget(ci)); var curveData = new MotionCurveData { - Type = curveTarget, - Id = id, - SegmentCount = segmentCount, - BaseSegmentIndex = segOffset, - ParamFadeIn = fadeIn, - ParamFadeOut = fadeOut, - Points = allPoints.ToArray(), - SegmentTypes = segTypesFlat + Type = target, + Id = _motionJson.GetCurveId(ci), + BaseSegmentIndex = segments.Count, + // -1 = время фейда кривой не задано в json (как в SDK). + FadeInTime = _motionJson.GetCurveFadeInTime(ci), + FadeOutTime = _motionJson.GetCurveFadeOutTime(ci), }; - segOffset += segmentCount; + _motionJson.GetCurveSegments(ci, out var rawSegments); + curveData.SegmentCount = ParseSegmentsInternal(rawSegments, points, segments); - switch (curveTarget) + switch (target) { - case CubismMotionCurveTarget.Model: - _modelCurves.Add(curveData); break; - case CubismMotionCurveTarget.Parameter: - _parameterCurves.Add(curveData); break; - case CubismMotionCurveTarget.PartOpacity: - _partOpacityCurves.Add(curveData); break; + case CubismMotionCurveTarget.Model: _modelCurves.Add(curveData); break; + case CubismMotionCurveTarget.Parameter: _parameterCurves.Add(curveData); break; + case CubismMotionCurveTarget.PartOpacity: _partOpacityCurves.Add(curveData); break; } } + + _points = points.ToArray(); + _segments = segments.ToArray(); } - private int ParseSegments(float[] raw, float[] segsFlat, CubismMotionSegmentType[] segTypesFlat, - int segOffset, List points) + /// + /// Разбор массива Segments одной кривой — порт 1:1 цикла CubismMotion::Parse. + /// Формат motion3.json: [t0, v0, type, t, v, type, t, v, v, v(bezier)...]: + /// первые два элемента — стартовая точка кривой, дальше для каждого сегмента + /// маркер типа и его точки (Linear/Stepped/InverseStepped — 1 точка, Bezier — 3). + /// Возвращает число сегментов кривой; точки и сегменты дописываются в общие списки. + /// + internal static int ParseSegmentsInternal(float[] raw, List points, List segments) { int count = 0; - int i = 0; - while (i < raw.Length) + int segmentPosition = 0; + while (segmentPosition < raw.Length) { - var segType = (CubismMotionSegmentType)(int)raw[i++]; - - if (count == 0) + int basePointIndex; + if (segmentPosition == 0) { - // First "segment" is actually the starting point - points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] }); - continue; + // Первая точка кривой: время и значение — не маркер типа. + basePointIndex = points.Count; + points.Add(new CubismMotionPoint { Time = raw[segmentPosition], Value = raw[segmentPosition + 1] }); + segmentPosition += 2; + } + else + { + // Сегменты продолжаются от последней точки предыдущего сегмента. + basePointIndex = points.Count - 1; } - segTypesFlat[segOffset + count - 1] = segType; - - switch (segType) + var segmentType = (CubismMotionSegmentType)(int)raw[segmentPosition]; + switch (segmentType) { case CubismMotionSegmentType.Linear: case CubismMotionSegmentType.Stepped: case CubismMotionSegmentType.InverseStepped: - points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] }); + points.Add(new CubismMotionPoint { Time = raw[segmentPosition + 1], Value = raw[segmentPosition + 2] }); + segmentPosition += 3; 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++] }); + points.Add(new CubismMotionPoint { Time = raw[segmentPosition + 1], Value = raw[segmentPosition + 2] }); + points.Add(new CubismMotionPoint { Time = raw[segmentPosition + 3], Value = raw[segmentPosition + 4] }); + points.Add(new CubismMotionPoint { Time = raw[segmentPosition + 5], Value = raw[segmentPosition + 6] }); + segmentPosition += 7; break; + + default: + // SDK: CSM_ASSERT(0). Неизвестный тип — останавливаем разбор кривой, + // чтобы не зациклиться на некорректных данных. + return count; } + segments.Add(new MotionSegmentData { BasePointIndex = basePointIndex, SegmentType = segmentType }); count++; } - return count - 1; + return count; + } + + /// + /// Порт CubismMotion::EvaluateCurve (анонимный namespace CubismMotion.cpp). + /// Сегмент ищется по времени ПЕРВОЙ точки следующего сегмента; если время за + /// пределами всех сегментов — значение последней точки, а при зацикленном V2 + /// (isCorrection) — интерполяция конца кривой к её началу (CorrectEndPoint). + /// + internal static float EvaluateCurveInternal( + MotionCurveData curve, + CubismMotionPoint[] points, + MotionSegmentData[] segments, + float time, + bool isCorrection, + float endTime, + bool areBeziersRestricted) + { + int target = -1; + int totalSegmentCount = curve.BaseSegmentIndex + curve.SegmentCount; + int pointPosition = 0; + + for (int i = curve.BaseSegmentIndex; i < totalSegmentCount; i++) + { + // Первая точка следующего сегмента = конечная точка текущего. + pointPosition = segments[i].BasePointIndex + + (segments[i].SegmentType == CubismMotionSegmentType.Bezier ? 3 : 1); + + if (points[pointPosition].Time > time) + { + target = i; + break; + } + } + + if (target == -1) + { + if (isCorrection && time < endTime) + { + // Переход конец -> начало при зацикливании (поведение V2). + return CorrectEndPointInternal( + segments[totalSegmentCount - 1].SegmentType, + points, + segments[curve.BaseSegmentIndex].BasePointIndex, + pointPosition, + time, + endTime); + } + + return points[pointPosition].Value; + } + + return EvaluateSegmentInternal(segments[target].SegmentType, points, segments[target].BasePointIndex, time, areBeziersRestricted); + } + + /// + /// Порт CorrectEndPoint: лерп от конечной точки кривой к начальной, + /// чьё время искусственно продлевается до endTime (границы цикла). + /// + internal static float CorrectEndPointInternal( + CubismMotionSegmentType lastSegmentType, + CubismMotionPoint[] points, + int beginIndex, + int endIndex, + float time, + float endTime) + { + CubismMotionPoint end = points[endIndex]; + CubismMotionPoint begin = points[beginIndex]; + begin.Time = endTime; + + switch (lastSegmentType) + { + case CubismMotionSegmentType.Stepped: + return end.Value; + case CubismMotionSegmentType.InverseStepped: + return begin.Value; + default: // Linear / Bezier / прочее — линейная интерполяция + return LinearEvaluatePoints(end, begin, time); + } + } + + /// Диспетчер оценки одного сегмента — порт таблички SegmentType -> Evaluate. + internal static float EvaluateSegmentInternal( + CubismMotionSegmentType segmentType, + CubismMotionPoint[] points, + int basePointIndex, + float time, + bool areBeziersRestricted) + { + switch (segmentType) + { + case CubismMotionSegmentType.Linear: + return LinearEvaluateInternal(points, basePointIndex, time); + + case CubismMotionSegmentType.Bezier: + return areBeziersRestricted + ? BezierEvaluateBinarySearchInternal(points, basePointIndex, time) + : BezierEvaluateCardanoInternal(points, basePointIndex, time); + + case CubismMotionSegmentType.Stepped: + return points[basePointIndex].Value; + + case CubismMotionSegmentType.InverseStepped: + return points[basePointIndex + 1].Value; + + default: + return LinearEvaluateInternal(points, basePointIndex, time); + } + } + + /// Порт LerpPoints: лерп времени и значения пары контрольных точек. + private static CubismMotionPoint LerpPoints(CubismMotionPoint a, CubismMotionPoint b, float t) + { + return new CubismMotionPoint + { + Time = a.Time + (b.Time - a.Time) * t, + Value = a.Value + (b.Value - a.Value) * t, + }; + } + + /// Порт LinearEvaluate: лерп пары точек, t не опускается ниже 0. + internal static float LinearEvaluatePoints(CubismMotionPoint p0, CubismMotionPoint p1, float time) + { + float t = (time - p0.Time) / (p1.Time - p0.Time); + if (t < 0f) t = 0f; + return p0.Value + (p1.Value - p0.Value) * t; + } + + internal static float LinearEvaluateInternal(CubismMotionPoint[] points, int baseIndex, float time) => + LinearEvaluatePoints(points[baseIndex], points[baseIndex + 1], time); + + /// Порт BezierEvaluate (restricted/старые безье): де Кастельо по параметру t из времени. + internal static float BezierEvaluateInternal(CubismMotionPoint[] points, int baseIndex, float time) + { + float t = (time - points[baseIndex].Time) / (points[baseIndex + 3].Time - points[baseIndex].Time); + if (t < 0f) t = 0f; + + CubismMotionPoint p01 = LerpPoints(points[baseIndex], points[baseIndex + 1], t); + CubismMotionPoint p12 = LerpPoints(points[baseIndex + 1], points[baseIndex + 2], t); + CubismMotionPoint p23 = LerpPoints(points[baseIndex + 2], points[baseIndex + 3], t); + + CubismMotionPoint p012 = LerpPoints(p01, p12, t); + CubismMotionPoint p123 = LerpPoints(p12, p23, t); + + return LerpPoints(p012, p123, t).Value; + } + + /// Порт BezierEvaluateBinarySearch: деление отрезка параметра пополам (x_error = 0.01, 20 итераций). + internal static float BezierEvaluateBinarySearchInternal(CubismMotionPoint[] points, int baseIndex, float time) + { + const float xError = 0.01f; + + float x = time; + float x1 = points[baseIndex].Time; + float x2 = points[baseIndex + 3].Time; + float cx1 = points[baseIndex + 1].Time; + float cx2 = points[baseIndex + 2].Time; + + float ta = 0f; + float tb = 1f; + float t = 0f; + int i = 0; + + for (; i < 20; i++) + { + if (x < x1 + xError) + { + t = ta; + break; + } + + if (x2 - xError < x) + { + t = tb; + break; + } + + float centerx = (cx1 + cx2) * 0.5f; + cx1 = (x1 + cx1) * 0.5f; + cx2 = (x2 + cx2) * 0.5f; + float ctrlx12 = (cx1 + centerx) * 0.5f; + float ctrlx21 = (cx2 + centerx) * 0.5f; + centerx = (ctrlx12 + ctrlx21) * 0.5f; + + if (x < centerx) + { + tb = (ta + tb) * 0.5f; + if (centerx - xError < x) + { + t = tb; + break; + } + + x2 = centerx; + cx2 = ctrlx12; + } + else + { + ta = (ta + tb) * 0.5f; + if (x < centerx + xError) + { + t = ta; + break; + } + + x1 = centerx; + cx1 = ctrlx21; + } + } + + if (i == 20) + t = (ta + tb) * 0.5f; + + if (t < 0f) t = 0f; + if (t > 1f) t = 1f; + + CubismMotionPoint p01 = LerpPoints(points[baseIndex], points[baseIndex + 1], t); + CubismMotionPoint p12 = LerpPoints(points[baseIndex + 1], points[baseIndex + 2], t); + CubismMotionPoint p23 = LerpPoints(points[baseIndex + 2], points[baseIndex + 3], t); + + CubismMotionPoint p012 = LerpPoints(p01, p12, t); + CubismMotionPoint p123 = LerpPoints(p12, p23, t); + + return LerpPoints(p012, p123, t).Value; + } + + /// + /// Порт BezierEvaluateCardanoInterpretation: параметр t ищется решением + /// кубического уравнения по времени (алгоритм Кардано), затем де Кастельо. + /// + internal static float BezierEvaluateCardanoInternal(CubismMotionPoint[] points, int baseIndex, float time) + { + float x1 = points[baseIndex].Time; + float x2 = points[baseIndex + 3].Time; + float cx1 = points[baseIndex + 1].Time; + float cx2 = points[baseIndex + 2].Time; + + float a = x2 - 3f * cx2 + 3f * cx1 - x1; + float b = 3f * cx2 - 6f * cx1 + 3f * x1; + float c = 3f * cx1 - 3f * x1; + float d = x1 - time; + + float t = CardanoAlgorithmForBezierInternal(a, b, c, d); + + CubismMotionPoint p01 = LerpPoints(points[baseIndex], points[baseIndex + 1], t); + CubismMotionPoint p12 = LerpPoints(points[baseIndex + 1], points[baseIndex + 2], t); + CubismMotionPoint p23 = LerpPoints(points[baseIndex + 2], points[baseIndex + 3], t); + + CubismMotionPoint p012 = LerpPoints(p01, p12, t); + CubismMotionPoint p123 = LerpPoints(p12, p23, t); + + return LerpPoints(p012, p123, t).Value; + } + + /// + /// Порт CubismMath::CardanoAlgorithmForBezier — решение кубического уравнения + /// по формулам Кардано с выбором корня, ближайшего к 0.5, и зажимом в [0,1]. + /// + internal static float CardanoAlgorithmForBezierInternal(float a, float b, float c, float d) + { + if (MathF.Abs(a) < MathEpsilon) + return RangeFInternal(QuadraticEquationInternal(b, c, d), 0f, 1f); + + float ba = b / a; + float ca = c / a; + float da = d / a; + + float p = (3f * ca - ba * ba) / 3f; + float p3 = p / 3f; + float q = (2f * ba * ba * ba - 9f * ba * ca + 27f * da) / 27f; + float q2 = q / 2f; + float discriminant = q2 * q2 + p3 * p3 * p3; + + const float center = 0.5f; + const float threshold = center + 0.01f; + + if (discriminant < 0f) + { + float mp3 = -p / 3f; + float mp33 = mp3 * mp3 * mp3; + float r = MathF.Sqrt(mp33); + float t = -q / (2f * r); + float cosphi = RangeFInternal(t, -1f, 1f); + float phi = MathF.Acos(cosphi); + float crtr = MathF.Cbrt(r); + float t1 = 2f * crtr; + + float root1 = t1 * MathF.Cos(phi / 3f) - ba / 3f; + if (MathF.Abs(root1 - center) < threshold) + return RangeFInternal(root1, 0f, 1f); + + float root2 = t1 * MathF.Cos((phi + 2f * MathF.PI) / 3f) - ba / 3f; + if (MathF.Abs(root2 - center) < threshold) + return RangeFInternal(root2, 0f, 1f); + + float root3 = t1 * MathF.Cos((phi + 4f * MathF.PI) / 3f) - ba / 3f; + return RangeFInternal(root3, 0f, 1f); + } + + if (discriminant == 0f) + { + float u1 = q2 < 0f ? MathF.Cbrt(-q2) : -MathF.Cbrt(q2); + + float root1 = 2f * u1 - ba / 3f; + if (MathF.Abs(root1 - center) < threshold) + return RangeFInternal(root1, 0f, 1f); + + float root2 = -u1 - ba / 3f; + return RangeFInternal(root2, 0f, 1f); + } + + float sd = MathF.Sqrt(discriminant); + float uc = MathF.Cbrt(sd - q2); + float vc = MathF.Cbrt(sd + q2); + return RangeFInternal(uc - vc - ba / 3f, 0f, 1f); + } + + /// Порт CubismMath::QuadraticEquation — решение вырожденного (a ≈ 0) случая. + private static float QuadraticEquationInternal(float a, float b, float c) + { + if (MathF.Abs(a) < MathEpsilon) + { + if (MathF.Abs(b) < MathEpsilon) + return -c; + return -c / b; + } + + return -(b + MathF.Sqrt(b * b - 4f * a * c)) / (2f * a); + } + + /// Порт CubismMath::RangeF — зажим значения в [min, max]. + private static float RangeFInternal(float value, float min, float max) + { + if (value < min) value = min; + else if (value > max) value = max; + return value; + } + + /// Порт зажима model->SetParameterValue: значение параметра в границах [min, max]. + private static float ClampParameterValue(float value, float min, float max) + { + if (value < min) return min; + if (value > max) return max; + return value; } private static CubismMotionCurveTarget ParseTarget(string target) => target switch @@ -421,9 +838,9 @@ namespace Cthangover.Live2D.Cubism.Framework } /// - /// 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 data for a single parsed motion curve. Хранит тип цели, ID, + /// диапазон сегментов в общем плоском массиве (BaseSegmentIndex/SegmentCount) + /// и пер-кривые времена фейда; значения < 0 означают «не задано» (как в SDK). /// internal class MotionCurveData { @@ -431,9 +848,7 @@ namespace Cthangover.Live2D.Cubism.Framework public string Id; public int SegmentCount; public int BaseSegmentIndex; - public float ParamFadeIn; - public float ParamFadeOut; - public CubismMotionPoint[] Points; - public CubismMotionSegmentType[] SegmentTypes; + public float FadeInTime; + public float FadeOutTime; } } diff --git a/source_code/Cubism/Framework/CubismMotionInternal.cs b/source_code/Cubism/Framework/CubismMotionInternal.cs index 2caf7a3..364fcd6 100644 --- a/source_code/Cubism/Framework/CubismMotionInternal.cs +++ b/source_code/Cubism/Framework/CubismMotionInternal.cs @@ -18,6 +18,22 @@ namespace Cthangover.Live2D.Cubism.Framework /// public enum CubismMotionSegmentType { Linear = 0, Bezier = 1, Stepped = 2, InverseStepped = 3 } + /// + /// Данные одного сегмента кривой — порт CubismMotionSegment из CubismMotionInternal.hpp: + /// индекс первой контрольной точки в общем плоском массиве точек и тип интерполяции. + /// Соседние сегменты кривой разделяют общую точку: BasePointIndex следующего сегмента + /// равен индексу последней точки предыдущего, а оценка кривой ищет сегмент по времени + /// первой точки СЛЕДУЮЩЕГО сегмента. + /// + internal struct MotionSegmentData + { + /// Индекс первой контрольной точки сегмента в CubismMotion._points. + public int BasePointIndex; + + /// Тип сегмента (Linear/Bezier/Stepped/InverseStepped). + public CubismMotionSegmentType SegmentType; + } + /// /// A time-value pair representing a keyframe or control point on a motion curve. /// diff --git a/source_code/Cubism/Framework/CubismMotionJson.cs b/source_code/Cubism/Framework/CubismMotionJson.cs index 3566daf..89769ab 100644 --- a/source_code/Cubism/Framework/CubismMotionJson.cs +++ b/source_code/Cubism/Framework/CubismMotionJson.cs @@ -9,7 +9,7 @@ namespace Cthangover.Live2D.Cubism.Framework /// 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 . + /// segment-type markers and point data), decoded by CubismMotion.ParseSegmentsInternal. /// public class CubismMotionJson { @@ -40,7 +40,7 @@ namespace Cthangover.Live2D.Cubism.Framework _duration = _meta.GetProperty("Duration").GetSingle(); _isLoop = _meta.TryGetProperty("Loop", out var loop) && loop.GetBoolean(); - _fps = _meta.TryGetProperty("Fps", out var fpsEl) ? fpsEl.GetSingle() : 30f; + _fps = _meta.TryGetProperty("Fps", out var fpsEl) && fpsEl.ValueKind == JsonValueKind.Number ? 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; @@ -75,13 +75,13 @@ namespace Cthangover.Live2D.Cubism.Framework /// Motion-level fade-in time in seconds, or -1 if not specified. /// public float GetFadeInTime() => - _meta.TryGetProperty("FadeInTime", out var v) ? v.GetSingle() : -1f; + _meta.TryGetProperty("FadeInTime", out var v) && v.ValueKind == JsonValueKind.Number ? 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; + _meta.TryGetProperty("FadeOutTime", out var v) && v.ValueKind == JsonValueKind.Number ? v.GetSingle() : -1f; /// /// Returns the target string for the curve at the given index @@ -100,18 +100,20 @@ namespace Cthangover.Live2D.Cubism.Framework /// 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; + _curves[index].TryGetProperty("FadeInTime", out var v) && v.ValueKind == JsonValueKind.Number ? 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; + _curves[index].TryGetProperty("FadeOutTime", out var v) && v.ValueKind == JsonValueKind.Number ? 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. + /// Формат Segments из motion3.json: [t0, v0, type, t, v, ...] — первые ДВА + /// элемента это стартовая точка кривой (время, значение), далее для каждого + /// сегмента маркер типа и его точки (Linear/Stepped/InverseStepped — 1 точка, + /// Bezier — 3). Разбор: . /// public void GetCurveSegments(int index, out float[] segments) { diff --git a/source_code/Cubism/Framework/CubismMotionQueueEntry.cs b/source_code/Cubism/Framework/CubismMotionQueueEntry.cs index 8a6613c..42531da 100644 --- a/source_code/Cubism/Framework/CubismMotionQueueEntry.cs +++ b/source_code/Cubism/Framework/CubismMotionQueueEntry.cs @@ -1,33 +1,54 @@ +using System; +using System.Collections.Generic; + 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 + /// lifecycle flags (available, started, 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 motion playback begins. -1 until the first update. + public float StartTime = -1f; /// /// Absolute user time when playback ends. -1 for infinite (looping) motions. /// - public float EndTime; + public float EndTime = -1f; /// 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; + public bool IsAvailable = true; /// Flag set when playback completes. Checked by the queue manager during cleanup. public bool IsFinished; + /// Flag set on the first update — порт CubismMotionQueueEntry::_started. + public bool IsStarted; + + /// Время последнего кадра (состояние), порт CubismMotionQueueEntry::SetState. + public float StateTime; + + /// Вес последнего кадра (состояние), порт CubismMotionQueueEntry::SetState. + public float StateWeight; + + /// Абсолютное время последней проверки событий UserData (сдвигается каждый кадр). + public float LastCheckEventTime; + + /// Длительность фейда-аута, назначенного при вытеснении новым моушеном. + public float FadeOutSeconds; + + /// Флаг вытеснения: фейд-аут начат, EndTime будет назначен при следующем обновлении. + public bool IsTriggeredFadeOut; + /// The motion instance driving parameter updates. public ACubismMotion Motion; @@ -45,29 +66,70 @@ namespace Cthangover.Live2D.Cubism.Framework { Motion = motion; AutoDelete = autoDelete; - IsAvailable = true; + } + + /// + /// Порт CubismMotionQueueEntry::SetFadeout: помечает запись для фейда-аута + /// (вызывается очередью, когда стартует новый моушен). + /// + public void SetFadeout(float fadeOutSeconds) + { + FadeOutSeconds = fadeOutSeconds; + IsTriggeredFadeOut = true; + } + + /// + /// Порт CubismMotionQueueEntry::StartFadeout: назначает EndTime, сокращая его, + /// если фейд-аут уже был запущен раньше и раньше должен закончиться. + /// + public void StartFadeout(float fadeOutSeconds, float userTimeSeconds) + { + float newEndTimeSeconds = userTimeSeconds + fadeOutSeconds; + IsTriggeredFadeOut = true; + + if (EndTime < 0f || newEndTimeSeconds < EndTime) + { + EndTime = newEndTimeSeconds; + } + } + + /// Порт CubismMotionQueueEntry::SetState — сохраняет время и вес последнего кадра. + public void SetState(float timeSeconds, float weight) + { + StateTime = timeSeconds; + StateWeight = weight; } } /// /// 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. + /// Поддержка вытеснения по приоритету (ReserveMotion), параллельного проигрывания + /// и автосборки завершённых записей. Порт CubismMotionQueueManager из SDK 5-r.5: + /// + /// - StartMotion назначает всем прежним записям фейд-аут; + /// - каждый кадр DoUpdateMotion вызывает , + /// затем проверяет события UserData () и + /// удаляет finished-записи; + /// - колбэком завершения владеет сам моушен: очередь его НЕ дублирует. /// public class CubismMotionQueueManager { - private readonly System.Collections.Generic.List _entries = new(); + private readonly List _entries = new(); + private readonly List _firedEventValues = new(); private int _reservedPriority = -1; + /// + /// Событие, вызываемое для каждого сработавшего UserData-события за кадр + /// (порт _eventCallback из CubismMotionQueueManager). + /// + public event Action EventFired; + + /// + /// События, собранные за последний кадр DoUpdateMotion по всем записям очереди. + /// Список очищается в начале каждого обновления — читайте его сразу после вызова. + /// + public IReadOnlyList FiredEventValues => _firedEventValues; + /// /// Sets the reserved priority threshold. /// Motions with priority below this are rejected by . @@ -89,21 +151,27 @@ namespace Cthangover.Live2D.Cubism.Framework } /// - /// Creates and enqueues a motion for playback. - /// Instantiates a , calls - /// on it, and - /// adds it to the queue. Returns the entry for optional tracking. + /// Enqueues a motion for playback. Прежним записям назначается фейд-аут + /// (SetFadeout), после чего они плавно завершаются; сама запись инициализируется + /// при первом кадре . + /// Параметр userTimeSeconds оставлен для совместимости вызовов. /// /// The motion to play. /// Whether to auto-remove on finish. - /// Current user time for start time calculation. + /// Current user time (не используется, порт сигнатуры). public CubismMotionQueueEntry StartMotion(ACubismMotion motion, bool autoDelete, float userTimeSeconds) { if (motion == null) return null; + // Порт CubismMotionQueueManager::StartMotion: прежние моушены вытесняются фейд-аутом. + foreach (var existing in _entries) + { + if (existing == null) continue; + existing.SetFadeout(existing.Motion?.FadeOutSeconds ?? 0f); + } + var entry = new CubismMotionQueueEntry(motion, autoDelete); - motion.SetupMotionQueueEntry(entry, userTimeSeconds); _entries.Add(entry); return entry; } @@ -123,40 +191,68 @@ namespace Cthangover.Live2D.Cubism.Framework public bool IsFinished() { foreach (var entry in _entries) - if (!entry.IsFinished) + if (entry != null && !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. + /// Per-frame update — порт CubismMotionQueueManager::DoUpdateMotion. + /// Для каждой записи: UpdateParameters (фейд + применение значений + финализация + /// IsFinished самим моушеном), сборUserData-событий за кадр, затем удаление + /// finished-записей и старт отложенного фейда-аута вытесненных. + /// Обход прямой, как в SDK: моушены применяются от старых к новым. /// public void DoUpdateMotion(CubismNativeModel model, float userTimeSeconds) { - for (int i = _entries.Count - 1; i >= 0; i--) + _firedEventValues.Clear(); + + int i = 0; + while (i < _entries.Count) { - var entry = _entries[i]; - if (!entry.IsAvailable || entry.IsFinished) + CubismMotionQueueEntry entry = _entries[i]; + + if (entry == null) { - if (entry.AutoDelete) - _entries.RemoveAt(i); + _entries.RemoveAt(i); continue; } - var weight = entry.Motion.UpdateFadeWeight(entry, userTimeSeconds); - entry.Motion.DoUpdateParameters(model, userTimeSeconds, weight, entry); - - if (entry.Motion.GetDuration() > 0f) + ACubismMotion motion = entry.Motion; + if (motion == null) { - if (userTimeSeconds >= entry.EndTime && !entry.Motion.IsLoop) - { - entry.IsFinished = true; - entry.Motion.OnFinishedMotion?.Invoke(entry.Motion); - } + _entries.RemoveAt(i); + continue; } + + // ------ Применение значений (финализация — внутри моушена/обёртки) ------ + motion.UpdateParameters(model, entry, userTimeSeconds); + + // ------ Проверка UserData-событий за кадр ------ + var fired = motion.GetFiredEvent( + entry.LastCheckEventTime - entry.StartTime, + userTimeSeconds - entry.StartTime); + + for (int e = 0; e < fired.Count; e++) + EventFired?.Invoke(motion, fired[e]); + + _firedEventValues.AddRange(fired); + entry.LastCheckEventTime = userTimeSeconds; + + // ------ Удаление завершённых записей ------ + if (entry.IsFinished) + { + _entries.RemoveAt(i); + continue; + } + + // Фейд-аут, запланированный при вытеснении, стартует после обновления. + if (entry.IsTriggeredFadeOut) + { + entry.StartFadeout(entry.FadeOutSeconds, userTimeSeconds); + } + + i++; } } } diff --git a/source_code/Cubism/Framework/CubismNativeModel.cs b/source_code/Cubism/Framework/CubismNativeModel.cs index 5558bb2..8cab8e5 100644 --- a/source_code/Cubism/Framework/CubismNativeModel.cs +++ b/source_code/Cubism/Framework/CubismNativeModel.cs @@ -29,6 +29,34 @@ namespace Cthangover.Live2D.Cubism.Framework private GCHandle _mocAlignedHandle; private GCHandle _modelAlignedHandle; + // The Core stores the raw function pointer, so the delegate must live + // for the whole process — a collected delegate would crash native code. + private static readonly CubismCoreBindings.CsmLogDelegate NativeLogDelegate = OnNativeLog; + private static bool _nativeLogWired; + + static CubismNativeModel() + { + WireNativeLog(); + } + + /// + /// Routes native Core log output (the only channel for native-side errors: + /// invalid moc3, unsupported version, alignment failures) into GameLogger. + /// + private static void WireNativeLog() + { + if (_nativeLogWired) return; + _nativeLogWired = true; + CubismCoreBindings.csmSetLogFunction(NativeLogDelegate); + } + + private static void OnNativeLog(IntPtr messagePtr) + { + if (messagePtr == IntPtr.Zero) return; + string message = Marshal.PtrToStringAnsi(messagePtr) ?? string.Empty; + GameLogger.Log("LIVE2D-CORE", message); + } + private int _drawableCount; private int _parameterCount; @@ -67,38 +95,95 @@ namespace Cthangover.Live2D.Cubism.Framework /// public unsafe void LoadMoc(byte[] mocBytes) { + if (IsLoaded) + { + // Guard against a silent native-memory leak: handles are pinned and the + // previous moc/model are never freed, so a second LoadMoc on the same + // instance is always a bug in the caller. + throw new InvalidOperationException( + "LoadMoc: a moc is already loaded in this CubismNativeModel instance. " + + "Create a new instance to load another model."); + } + 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) + try { - GameLogger.Log("LIVE2D", "LoadMoc: csmReviveMocInPlace failed — .moc3 data is invalid or corrupted", LogLevel.Error); - throw new InvalidOperationException("csmReviveMocInPlace failed"); + 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); + + // csmReviveMocInPlace (verified by disassembly of the shipped DLL) hard-requires + // size % 64 == 0 and returns failure with '"size" is invalid.' otherwise. Cubism + // Editor always exports moc3 files whose length is a multiple of 64, but we do not + // rely on that: the buffer is already zero-initialized and 64-byte padded, so we + // pass the padded size — the Core reads only the real content, zero padding is safe. + uint mocBufferSize = (uint)alignedMocSize; + + // Validate the moc3 buffer against the loaded Core before reviving it. + // Both checks use Core functions that were previously declared but never + // called — this gives diagnosable errors instead of a bare + // "csmReviveMocInPlace failed" for wrong-version or corrupted files. + uint mocVersion = CubismCoreBindings.csmGetMocVersion((void*)alignedMocPtr, mocBufferSize); + uint coreVersion = CubismCoreBindings.csmGetLatestMocVersion(); + if (mocVersion == 0) + { + GameLogger.Log("LIVE2D", "LoadMoc: buffer is not a recognized .moc3 file (csmGetMocVersion returned 0)", LogLevel.Error); + throw new InvalidOperationException("The .moc3 buffer is not a recognized moc3 file"); + } + if (mocVersion > coreVersion) + { + GameLogger.Log("LIVE2D", + $"LoadMoc: .moc3 version {mocVersion} is newer than Cubism Core {coreVersion} — ship a newer Live2DCubismCore.dll", + LogLevel.Error); + throw new InvalidOperationException( + $".moc3 version {mocVersion} is not supported by Cubism Core {coreVersion}"); + } + + if (CubismCoreBindings.csmHasMocConsistency((void*)alignedMocPtr, mocBufferSize) != 1) + { + GameLogger.Log("LIVE2D", "LoadMoc: .moc3 failed the consistency check (csmHasMocConsistency != 1) — data is corrupted", LogLevel.Error); + throw new InvalidOperationException(".moc3 data is inconsistent (csmHasMocConsistency failed)"); + } + + IntPtr moc = CubismCoreBindings.csmReviveMocInPlace( + (void*)alignedMocPtr, mocBufferSize); + 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"); + } } - - 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) + catch { - GameLogger.Log("LIVE2D", "LoadMoc: csmInitializeModelInPlace failed — model memory allocation failed", LogLevel.Error); - throw new InvalidOperationException("csmInitializeModelInPlace failed"); + // Any failure after the Alloc calls must release the pinned buffers — + // otherwise the GCHandles (and the native memory they pin) leak for the + // rest of the process, because LoadMoc throws before Dispose is reachable. + if (_mocAlignedHandle.IsAllocated) _mocAlignedHandle.Free(); + if (_modelAlignedHandle.IsAllocated) _modelAlignedHandle.Free(); + _mocAlignedHandle = default; + _modelAlignedHandle = default; + throw; } var sizeInPixels = default(CubismCoreBindings.csmVector2); @@ -319,6 +404,39 @@ namespace Cthangover.Live2D.Cubism.Framework public unsafe IntPtr GetPartIds() => CubismCoreBindings.csmGetPartIds(_model); + // Snapshot of the parameter values for the Load/Save bracket + // (port of CubismModel.cpp:878-910). + private float[] _savedParameterValues; + + /// + /// Порт CubismModel::SaveParameters: сохраняет текущие значения параметров в снимок. + /// Вызывается сразу после фазы моушена, чтобы аддитивные слои (выражения, дыхание, + /// look-инъекция) каждый кадр стартовали от одного и того же базового состояния + /// и не накапливались кадр-за-кадром. + /// + public unsafe void SaveParameters() + { + if (_model == IntPtr.Zero) return; + var values = GetParameterValues(); + if (_savedParameterValues == null || _savedParameterValues.Length != _parameterCount) + _savedParameterValues = new float[_parameterCount]; + for (int i = 0; i < _parameterCount; i++) + _savedParameterValues[i] = values[i]; + } + + /// + /// Порт CubismModel::LoadParameters: восстанавливает значения параметров из снимка. + /// До первого SaveParameters ничего не делает (как в SDK). + /// + public unsafe void LoadParameters() + { + if (_model == IntPtr.Zero || _savedParameterValues == null) return; + var values = GetParameterValues(); + var count = System.Math.Min(_parameterCount, _savedParameterValues.Length); + for (int i = 0; i < count; i++) + values[i] = _savedParameterValues[i]; + } + /// /// Frees pinned memory handles. The native model handle is set to zero; /// the underlying CubismCore memory was allocated inline and lives in diff --git a/source_code/Cubism/Framework/CubismPhysics.cs b/source_code/Cubism/Framework/CubismPhysics.cs index 442b524..2581a6a 100644 --- a/source_code/Cubism/Framework/CubismPhysics.cs +++ b/source_code/Cubism/Framework/CubismPhysics.cs @@ -7,8 +7,9 @@ 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: + /// + /// Bit-exact port of Cubism SDK for Native 5-r.5 Physics/CubismPhysics.cpp. + /// Architecture (mirroring the native rig structures): /// /// holds the complete rig structure: /// sub-rigs, inputs, outputs, and particles @@ -19,26 +20,50 @@ namespace Cthangover.Live2D.Cubism.Framework /// 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. + /// + /// The simulation runs at the rig's configured FPS (from .physics3.json, + /// Meta.Fps; 0 means "step once per display frame"). Pendulum results are + /// kept as current/previous rig outputs and the value written to the model + /// parameters is interpolated between them by the remaining sub-frame time. + /// The pure math core (normalization, particle update, output value update) + /// lives in internal static methods operating on plain data for testability. /// public class CubismPhysics { + // ------------------------------------------------------------------ + // Physics constants (SDK CubismPhysics.cpp anonymous namespace). + // ------------------------------------------------------------------ private const float AirResistance = 5f; private const float MaximumWeight = 100f; + private const float MovementThreshold = 0.001f; + private const float MaxDeltaTime = 5f; + + /// SDK CubismMath::Pi (identical to MathF.PI as a csmFloat32). + private const float Pi = MathF.PI; private readonly CubismPhysicsRig _rig = new(); - private readonly List _currentOutputs = new(); - private readonly List _previousOutputs = new(); + + // Runtime options (SDK _options). Native defaults: gravity (0, -1), wind (0, 0). + private float _optionsGravityX; + private float _optionsGravityY = -1f; + private float _optionsWindX; + private float _optionsWindY; + + // Parameter value caches used for sub-frame input interpolation + // (SDK _parameterCaches / _parameterInputCaches). + private float[] _parameterCaches = Array.Empty(); + private float[] _parameterInputCaches = Array.Empty(); + + // Pendulum output history (SDK _currentRigOutputs / _previousRigOutputs, + // flattened to one slot per absolute output index). + private float[] _currentRigOutputs = Array.Empty(); + private float[] _previousRigOutputs = Array.Empty(); + 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. + /// with sub-rigs, inputs, outputs, and particles (SDK Parse + Initialize). /// public CubismPhysics(byte[] physics3JsonBytes) { @@ -47,319 +72,1058 @@ namespace Cthangover.Live2D.Cubism.Framework 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++) + int subRigCount = json.GetSubRigCount(); + var particles = new List(json.GetVertexCount()); + _rig.Inputs.Capacity = json.GetTotalInputCount(); + _rig.Outputs.Capacity = json.GetTotalOutputCount(); + + int inputIndex = 0, outputIndex = 0, particleIndex = 0; + for (int si = 0; si < subRigCount; si++) { var subRig = new CubismPhysicsSubRig { - InputCount = json.GetInputCount(si), - OutputCount = json.GetOutputCount(si), - ParticleCount = json.GetVertexCount(si), - BaseInputIndex = inputOffset, - BaseOutputIndex = outputOffset, - BaseParticleIndex = particleOffset + BaseInputIndex = inputIndex, + BaseOutputIndex = outputIndex, + BaseParticleIndex = particleIndex, }; json.GetNormalizationPosition(si, out subRig.NormalizationPosition); json.GetNormalizationAngle(si, out subRig.NormalizationAngle); - _rig.SubRigs.Add(subRig); - for (int ii = 0; ii < subRig.InputCount; ii++) + // Input + subRig.InputCount = json.GetInputCount(si); + for (int j = 0; j < subRig.InputCount; j++) { - 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 + SourceParameterIndex = -1, + Weight = json.GetInputWeight(si, j), + Reflect = json.GetInputReflect(si, j), + Type = ParseSource(json.GetInputType(si, j)), + ParameterId = json.GetInputSourceId(si, j), }); } + inputIndex += subRig.InputCount; - for (int oi = 0; oi < subRig.OutputCount; oi++) + // Output + subRig.OutputCount = json.GetOutputCount(si); + for (int j = 0; j < subRig.OutputCount; j++) { - 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 + DestinationParameterIndex = -1, + VertexIndex = json.GetOutputVertexIndex(si, j), + AngleScale = json.GetOutputAngleScale(si, j), + Weight = json.GetOutputWeight(si, j), + ParameterId = json.GetOutputsDestinationId(si, j), + Type = ParseSource(json.GetOutputType(si, j)), + Reflect = json.GetOutputReflect(si, j), + // TranslationScale stays (0, 0): native never loads a + // translation scale from physics3.json, so X/Y outputs + // scale by zero exactly like the native implementation. }); } + outputIndex += subRig.OutputCount; - 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++) + // Particle + subRig.ParticleCount = json.GetParticleCount(si); + for (int j = 0; j < subRig.ParticleCount; j++) { - 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 + json.GetParticlePosition(si, j, out var px, out var py); + particles.Add(new CubismPhysicsParticle { - InitialX = px, InitialY = py, - Mobility = mob, Delay = del, Acceleration = acc, Radius = rad, - PositionX = px, PositionY = py, - LastPositionX = px, LastPositionY = py - }; + InitialX = px, + InitialY = py, + Mobility = json.GetParticleMobility(si, j), + Delay = json.GetParticleDelay(si, j), + Acceleration = json.GetParticleAcceleration(si, j), + Radius = json.GetParticleRadius(si, j), + PositionX = px, + PositionY = py, + }); } + particleIndex += subRig.ParticleCount; - inputOffset += subRig.InputCount; - outputOffset += subRig.OutputCount; - particleOffset += subRig.ParticleCount; + _rig.SubRigs.Add(subRig); } - _currentOutputs.Capacity = _rig.Outputs.Count; - _previousOutputs.Capacity = _rig.Outputs.Count; - for (int i = 0; i < _rig.Outputs.Count; i++) + _rig.Particles = particles.ToArray(); + + _currentRigOutputs = new float[_rig.Outputs.Count]; + _previousRigOutputs = new float[_rig.Outputs.Count]; + + Initialize(); + + // SDK CubismPhysics::Create zeroes the rig gravity Y after parsing. + _rig.GravityY = 0f; + } + + /// + /// Lays out the particle chains at their rest positions + /// (SDK CubismPhysics::Initialize). + /// + private void Initialize() + { + foreach (var currentSetting in _rig.SubRigs) { - _currentOutputs.Add(0f); - _previousOutputs.Add(0f); + int baseParticleIndex = currentSetting.BaseParticleIndex; + + // Initialize the top of the strand. + _rig.Particles[baseParticleIndex].InitialX = 0f; + _rig.Particles[baseParticleIndex].InitialY = 0f; + _rig.Particles[baseParticleIndex].LastPositionX = 0f; + _rig.Particles[baseParticleIndex].LastPositionY = 0f; + _rig.Particles[baseParticleIndex].LastGravityX = 0f; + _rig.Particles[baseParticleIndex].LastGravityY = 1f; // (0, -1) with Y negated + _rig.Particles[baseParticleIndex].VelocityX = 0f; + _rig.Particles[baseParticleIndex].VelocityY = 0f; + _rig.Particles[baseParticleIndex].ForceX = 0f; + _rig.Particles[baseParticleIndex].ForceY = 0f; + + // Initialize the remaining particles along +Y by radius. + for (int i = 1; i < currentSetting.ParticleCount; ++i) + { + int idx = baseParticleIndex + i; + _rig.Particles[idx].InitialX = _rig.Particles[idx - 1].InitialX; + _rig.Particles[idx].InitialY = _rig.Particles[idx - 1].InitialY + _rig.Particles[idx].Radius; + _rig.Particles[idx].PositionX = _rig.Particles[idx].InitialX; + _rig.Particles[idx].PositionY = _rig.Particles[idx].InitialY; + _rig.Particles[idx].LastPositionX = _rig.Particles[idx].InitialX; + _rig.Particles[idx].LastPositionY = _rig.Particles[idx].InitialY; + _rig.Particles[idx].LastGravityX = 0f; + _rig.Particles[idx].LastGravityY = 1f; // (0, -1) with Y negated + _rig.Particles[idx].VelocityX = 0f; + _rig.Particles[idx].VelocityY = 0f; + _rig.Particles[idx].ForceX = 0f; + _rig.Particles[idx].ForceY = 0f; + } } } /// - /// Overrides gravity and wind at runtime. + /// Overrides gravity and wind at runtime (SDK SetOptions; these feed the + /// wind force during simulation and the parent gravity of angle outputs). /// public void SetOptions(float gravityX, float gravityY, float windX, float windY) { - _rig.GravityX = gravityX; - _rig.GravityY = gravityY; - _rig.WindX = windX; - _rig.WindY = windY; + _optionsGravityX = gravityX; + _optionsGravityY = gravityY; + _optionsWindX = windX; + _optionsWindY = 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. + /// Forces the physics to stabilize immediately (SDK CubismPhysics::Stabilization). + /// Particles snap to their rest positions along the current gravity + /// direction and the resolved outputs are written into the model. /// - public void Stabilization(CubismNativeModel model) + public unsafe void Stabilization(CubismNativeModel model) { - UpdatePhysics(model, true); + int parameterCount = model.ParameterCount; + float* parameterValues = model.GetParameterValues(); + float* parameterMaximumValues = model.GetParameterMaximumValues(); + float* parameterMinimumValues = model.GetParameterMinimumValues(); + float* parameterDefaultValues = model.GetParameterDefaultValues(); + + if (_parameterCaches.Length < parameterCount) + { + _parameterCaches = new float[parameterCount]; + } + if (_parameterInputCaches.Length < parameterCount) + { + _parameterInputCaches = new float[parameterCount]; + } + + for (int j = 0; j < parameterCount; ++j) + { + _parameterCaches[j] = parameterValues[j]; + _parameterInputCaches[j] = parameterValues[j]; + } + + foreach (var currentSetting in _rig.SubRigs) + { + float totalAngle = 0f; + float totalTranslationX = 0f, totalTranslationY = 0f; + + // Load input parameters. + for (int i = 0; i < currentSetting.InputCount; ++i) + { + int inputIndex = currentSetting.BaseInputIndex + i; + var input = _rig.Inputs[inputIndex]; + float weight = input.Weight / MaximumWeight; + + if (input.SourceParameterIndex == -1) + { + input.SourceParameterIndex = ResolveParameterIndex(model, input.ParameterId); + } + if (input.SourceParameterIndex == -1) + { + continue; + } + _rig.Inputs[inputIndex] = input; + + int source = input.SourceParameterIndex; + switch (input.Type) + { + case CubismPhysicsSource.X: + GetInputTranslationXFromNormalizedParameterValue( + ref totalTranslationX, ref totalTranslationY, ref totalAngle, + parameterValues[source], + parameterMinimumValues[source], + parameterMaximumValues[source], + parameterDefaultValues[source], + currentSetting.NormalizationPosition, + currentSetting.NormalizationAngle, + input.Reflect, weight); + break; + case CubismPhysicsSource.Y: + GetInputTranslationYFromNormalizedParameterValue( + ref totalTranslationX, ref totalTranslationY, ref totalAngle, + parameterValues[source], + parameterMinimumValues[source], + parameterMaximumValues[source], + parameterDefaultValues[source], + currentSetting.NormalizationPosition, + currentSetting.NormalizationAngle, + input.Reflect, weight); + break; + case CubismPhysicsSource.Angle: + GetInputAngleFromNormalizedParameterValue( + ref totalTranslationX, ref totalTranslationY, ref totalAngle, + parameterValues[source], + parameterMinimumValues[source], + parameterMaximumValues[source], + parameterDefaultValues[source], + currentSetting.NormalizationPosition, + currentSetting.NormalizationAngle, + input.Reflect, weight); + break; + } + + _parameterCaches[source] = parameterValues[source]; + } + + float radAngle = DegreesToRadian(-totalAngle); + + // NOTE: native computes Y from the already-updated X + // (bug-compatible with CubismPhysics.cpp Stabilization). + totalTranslationX = totalTranslationX * CosF(radAngle) - totalTranslationY * SinF(radAngle); + totalTranslationY = totalTranslationX * SinF(radAngle) + totalTranslationY * CosF(radAngle); + + // Calculate particles position. + UpdateParticlesForStabilization( + _rig.Particles, currentSetting.BaseParticleIndex, currentSetting.ParticleCount, + totalTranslationX, totalTranslationY, totalAngle, + _optionsWindX, _optionsWindY, + MovementThreshold * currentSetting.NormalizationPosition.Maximum); + + // Update output parameters. + for (int i = 0; i < currentSetting.OutputCount; ++i) + { + int outputIndex = currentSetting.BaseOutputIndex + i; + var output = _rig.Outputs[outputIndex]; + int particleIndex = output.VertexIndex; + + if (output.DestinationParameterIndex == -1) + { + output.DestinationParameterIndex = ResolveParameterIndex(model, output.ParameterId); + } + + if (particleIndex < 1 || particleIndex >= currentSetting.ParticleCount) + { + _rig.Outputs[outputIndex] = output; + continue; + } + + int vertex = currentSetting.BaseParticleIndex + particleIndex; + float translationX = _rig.Particles[vertex].PositionX - _rig.Particles[vertex - 1].PositionX; + float translationY = _rig.Particles[vertex].PositionY - _rig.Particles[vertex - 1].PositionY; + + float outputValue = GetOutputValue( + output.Type, translationX, translationY, + _rig.Particles, currentSetting.BaseParticleIndex, particleIndex, + output.Reflect, _optionsGravityX, _optionsGravityY); + + _currentRigOutputs[outputIndex] = outputValue; + _previousRigOutputs[outputIndex] = outputValue; + + if (output.DestinationParameterIndex != -1) + { + int destination = output.DestinationParameterIndex; + UpdateOutputParameterValue( + ref parameterValues[destination], + parameterMinimumValues[destination], + parameterMaximumValues[destination], + outputValue, ref output); + + _rig.Outputs[outputIndex] = output; + _parameterCaches[destination] = parameterValues[destination]; + } + } + } } /// - /// Evaluates physics for one display frame. + /// Evaluates physics for one display frame (SDK CubismPhysics::Evaluate). /// 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. + /// Inputs are linearly interpolated between the cached and live + /// parameter values at each step, and the output written to the model + /// parameters is interpolated between the previous and current pendulum + /// results by the remaining sub-frame time. /// Called from every frame. /// public unsafe void Evaluate(CubismNativeModel model, float deltaTimeSeconds) { + if (0f >= deltaTimeSeconds) + { + return; + } + + int parameterCount = model.ParameterCount; + float* parameterValues = model.GetParameterValues(); + float* parameterMaximumValues = model.GetParameterMaximumValues(); + float* parameterMinimumValues = model.GetParameterMinimumValues(); + float* parameterDefaultValues = model.GetParameterDefaultValues(); + _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) + if (_currentRemainTime > MaxDeltaTime) { - _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); + _currentRemainTime = 0f; } - 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++) + if (_parameterCaches.Length < parameterCount) { - var output = _rig.Outputs[oi]; - var interpolated = _previousOutputs[oi] * (1f - alpha) + _currentOutputs[oi] * alpha; - for (int pi = 0; pi < model.ParameterCount; pi++) + _parameterCaches = new float[parameterCount]; + } + if (_parameterInputCaches.Length < parameterCount) + { + _parameterInputCaches = new float[parameterCount]; + for (int j = 0; j < parameterCount; ++j) { - if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == output.ParameterId) + _parameterInputCaches[j] = parameterValues[j]; + } + } + + float physicsDeltaTime = _rig.Fps > 0f ? 1f / _rig.Fps : deltaTimeSeconds; + if (!(physicsDeltaTime > 0f)) + { + physicsDeltaTime = deltaTimeSeconds; + } + + // After the MaxDeltaTime clamp the loop can consume at most + // ceil(MaxDeltaTime / physicsDeltaTime) steps; cap it defensively so + // pathological Fps values cannot spin (SDK relies on well-formed data). + int maxSteps = (int)(MaxDeltaTime / physicsDeltaTime) + 2; + int steps = 0; + + while (_currentRemainTime >= physicsDeltaTime) + { + if (++steps > maxSteps) + { + _currentRemainTime = 0f; + break; + } + + // copyRigOutputs: _currentRigOutputs -> _previousRigOutputs. + Array.Copy(_currentRigOutputs, _previousRigOutputs, _currentRigOutputs.Length); + + // Linear interpolation between the cached input values and the + // live parameter values at each UpdateParticles timing. + // _parameterCaches propagates values between groups, which is + // why it is kept separate from _parameterInputCaches. + float inputWeight = physicsDeltaTime / _currentRemainTime; + for (int j = 0; j < parameterCount; ++j) + { + _parameterCaches[j] = _parameterInputCaches[j] * (1f - inputWeight) + parameterValues[j] * inputWeight; + _parameterInputCaches[j] = _parameterCaches[j]; + } + + foreach (var currentSetting in _rig.SubRigs) + { + float totalAngle = 0f; + float totalTranslationX = 0f, totalTranslationY = 0f; + + // Load input parameters. + for (int i = 0; i < currentSetting.InputCount; ++i) { - 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; + int inputIndex = currentSetting.BaseInputIndex + i; + var input = _rig.Inputs[inputIndex]; + float weight = input.Weight / MaximumWeight; + + if (input.SourceParameterIndex == -1) + { + input.SourceParameterIndex = ResolveParameterIndex(model, input.ParameterId); + } + if (input.SourceParameterIndex == -1) + { + continue; + } + _rig.Inputs[inputIndex] = input; + + int source = input.SourceParameterIndex; + switch (input.Type) + { + case CubismPhysicsSource.X: + GetInputTranslationXFromNormalizedParameterValue( + ref totalTranslationX, ref totalTranslationY, ref totalAngle, + _parameterCaches[source], + parameterMinimumValues[source], + parameterMaximumValues[source], + parameterDefaultValues[source], + currentSetting.NormalizationPosition, + currentSetting.NormalizationAngle, + input.Reflect, weight); + break; + case CubismPhysicsSource.Y: + GetInputTranslationYFromNormalizedParameterValue( + ref totalTranslationX, ref totalTranslationY, ref totalAngle, + _parameterCaches[source], + parameterMinimumValues[source], + parameterMaximumValues[source], + parameterDefaultValues[source], + currentSetting.NormalizationPosition, + currentSetting.NormalizationAngle, + input.Reflect, weight); + break; + case CubismPhysicsSource.Angle: + GetInputAngleFromNormalizedParameterValue( + ref totalTranslationX, ref totalTranslationY, ref totalAngle, + _parameterCaches[source], + parameterMinimumValues[source], + parameterMaximumValues[source], + parameterDefaultValues[source], + currentSetting.NormalizationPosition, + currentSetting.NormalizationAngle, + input.Reflect, weight); + break; + } } + + float radAngle = DegreesToRadian(-totalAngle); + + // NOTE: native computes Y from the already-updated X + // (bug-compatible with CubismPhysics.cpp Evaluate). + totalTranslationX = totalTranslationX * CosF(radAngle) - totalTranslationY * SinF(radAngle); + totalTranslationY = totalTranslationX * SinF(radAngle) + totalTranslationY * CosF(radAngle); + + // Calculate particles position. + UpdateParticles( + _rig.Particles, currentSetting.BaseParticleIndex, currentSetting.ParticleCount, + totalTranslationX, totalTranslationY, totalAngle, + _optionsWindX, _optionsWindY, + MovementThreshold * currentSetting.NormalizationPosition.Maximum, + physicsDeltaTime, AirResistance); + + // Update output parameters. + for (int i = 0; i < currentSetting.OutputCount; ++i) + { + int outputIndex = currentSetting.BaseOutputIndex + i; + var output = _rig.Outputs[outputIndex]; + int particleIndex = output.VertexIndex; + + if (output.DestinationParameterIndex == -1) + { + output.DestinationParameterIndex = ResolveParameterIndex(model, output.ParameterId); + } + + if (particleIndex < 1 || particleIndex >= currentSetting.ParticleCount) + { + _rig.Outputs[outputIndex] = output; + continue; + } + + int vertex = currentSetting.BaseParticleIndex + particleIndex; + float translationX = _rig.Particles[vertex].PositionX - _rig.Particles[vertex - 1].PositionX; + float translationY = _rig.Particles[vertex].PositionY - _rig.Particles[vertex - 1].PositionY; + + float outputValue = GetOutputValue( + output.Type, translationX, translationY, + _rig.Particles, currentSetting.BaseParticleIndex, particleIndex, + output.Reflect, _optionsGravityX, _optionsGravityY); + + _currentRigOutputs[outputIndex] = outputValue; + + if (output.DestinationParameterIndex != -1) + { + int destination = output.DestinationParameterIndex; + UpdateOutputParameterValue( + ref _parameterCaches[destination], + parameterMinimumValues[destination], + parameterMaximumValues[destination], + outputValue, ref output); + } + + _rig.Outputs[outputIndex] = output; + } + } + + _currentRemainTime -= physicsDeltaTime; + } + + float alpha = _currentRemainTime / physicsDeltaTime; + Interpolate(parameterValues, parameterMinimumValues, parameterMaximumValues, alpha); + } + + /// + /// Writes the interpolated pendulum results into the model parameters + /// (SDK CubismPhysics::Interpolate). Outputs whose destination was never + /// resolved are skipped. + /// + private unsafe void Interpolate( + float* parameterValues, float* parameterMinimumValues, float* parameterMaximumValues, float weight) + { + foreach (var currentSetting in _rig.SubRigs) + { + for (int i = 0; i < currentSetting.OutputCount; ++i) + { + int outputIndex = currentSetting.BaseOutputIndex + i; + var output = _rig.Outputs[outputIndex]; + + if (output.DestinationParameterIndex == -1) + { + continue; + } + + int destination = output.DestinationParameterIndex; + float interpolated = + _previousRigOutputs[outputIndex] * (1 - weight) + _currentRigOutputs[outputIndex] * weight; + + UpdateOutputParameterValue( + ref parameterValues[destination], + parameterMinimumValues[destination], + parameterMaximumValues[destination], + interpolated, ref output); + + _rig.Outputs[outputIndex] = output; } } } - private unsafe void UpdatePhysics(CubismNativeModel model, bool stabilization) + // ================================================================== + // Pure math core (internal static, no CubismNativeModel dependency — + // unit-testable against the native SDK formulas). + // ================================================================== + + /// Dispatches to the SDK value getter for an output type. + internal static float GetOutputValue( + CubismPhysicsSource type, float translationX, float translationY, + CubismPhysicsParticle[] particles, int baseParticleIndex, int particleIndex, + bool isInverted, float parentGravityX, float parentGravityY) + { + switch (type) + { + case CubismPhysicsSource.X: + return GetOutputTranslationX(translationX, isInverted); + case CubismPhysicsSource.Y: + return GetOutputTranslationY(translationY, isInverted); + case CubismPhysicsSource.Angle: + return GetOutputAngle(translationX, translationY, particles, baseParticleIndex, particleIndex, + isInverted, parentGravityX, parentGravityY); + default: + return GetOutputTranslationX(translationX, isInverted); + } + } + + internal static float GetOutputTranslationX(float translationX, bool isInverted) + { + float outputValue = translationX; + + if (isInverted) + { + outputValue *= -1f; + } + + return outputValue; + } + + internal static float GetOutputTranslationY(float translationY, bool isInverted) + { + float outputValue = translationY; + + if (isInverted) + { + outputValue *= -1f; + } + + return outputValue; + } + + /// + /// SDK GetOutputAngle: the parent gravity is the direction between the + /// two previous particles once the pendulum has at least two segments, + /// otherwise the negated rig gravity. + /// + internal static float GetOutputAngle( + float translationX, float translationY, + CubismPhysicsParticle[] particles, int baseParticleIndex, int particleIndex, + bool isInverted, float parentGravityX, float parentGravityY) + { + float gravityX, gravityY; + if (particleIndex >= 2) + { + gravityX = particles[baseParticleIndex + particleIndex - 1].PositionX + - particles[baseParticleIndex + particleIndex - 2].PositionX; + gravityY = particles[baseParticleIndex + particleIndex - 1].PositionY + - particles[baseParticleIndex + particleIndex - 2].PositionY; + } + else + { + gravityX = -parentGravityX; + gravityY = -parentGravityY; + } + + float outputValue = DirectionToRadian(gravityX, gravityY, translationX, translationY); + + if (isInverted) + { + outputValue *= -1f; + } + + return outputValue; + } + + /// SDK scale getter dispatch: X/Y use the (never loaded, i.e. zero) translation scale; Angle uses AngleScale. + internal static float GetOutputScale(CubismPhysicsSource type, float translationScaleX, float translationScaleY, float angleScale) + { + switch (type) + { + case CubismPhysicsSource.X: + return translationScaleX; + case CubismPhysicsSource.Y: + return translationScaleY; + default: + return angleScale; + } + } + + /// + /// SDK UpdateOutputParameterValue: scale the translation, clamp to the + /// parameter range (recording below-minimum/exceeded-maximum extremes), + /// then blend into the parameter by Output.Weight / 100. + /// + internal static void UpdateOutputParameterValue( + ref float parameterValue, float parameterValueMinimum, float parameterValueMaximum, + float translation, ref CubismPhysicsOutput output) + { + float outputScale = GetOutputScale(output.Type, output.TranslationScaleX, output.TranslationScaleY, output.AngleScale); + + float value = translation * outputScale; + + if (value < parameterValueMinimum) + { + if (value < output.ValueBelowMinimum) + { + output.ValueBelowMinimum = value; + } + + value = parameterValueMinimum; + } + else if (value > parameterValueMaximum) + { + if (value > output.ValueExceededMaximum) + { + output.ValueExceededMaximum = value; + } + + value = parameterValueMaximum; + } + + float weight = output.Weight / MaximumWeight; + + if (weight >= 1f) + { + parameterValue = value; + } + else + { + value = parameterValue * (1f - weight) + value * weight; + parameterValue = value; + } + } + + /// SDK GetRangeValue. + internal static float GetRangeValue(float min, float max) + { + float maxValue = MaxF(min, max); + float minValue = MinF(min, max); + + return AbsF(maxValue - minValue); + } + + /// SDK Sign. + internal static int Sign(float value) + { + if (value > 0f) + { + return 1; + } + if (value < 0f) + { + return -1; + } + + return 0; + } + + /// SDK GetDefaultValue: the middle of the parameter range. + internal static float GetDefaultValue(float min, float max) + { + float minValue = MinF(min, max); + return minValue + GetRangeValue(min, max) / 2f; + } + + /// + /// SDK NormalizeParameterValue: clamp the value to its parameter range, + /// map it piecewise across the normalization range relative to the + /// default middle, and invert when the input is not reflected + /// (native applies the sign flip to non-inverted values). + /// + internal static float NormalizeParameterValue( + float value, + float parameterMinimum, + float parameterMaximum, + float parameterDefault, + float normalizedMinimum, + float normalizedMaximum, + float normalizedDefault, + bool isInverted) + { + float result = 0f; + + float maxValue = MaxF(parameterMaximum, parameterMinimum); + if (maxValue < value) + { + value = maxValue; + } + + float minValue = MinF(parameterMaximum, parameterMinimum); + if (minValue > value) + { + value = minValue; + } + + float minNormValue = MinF(normalizedMinimum, normalizedMaximum); + float maxNormValue = MaxF(normalizedMinimum, normalizedMaximum); + float middleNormValue = normalizedDefault; + + float middleValue = GetDefaultValue(minValue, maxValue); + float paramValue = value - middleValue; + + switch (Sign(paramValue)) + { + case 1: + { + float nLength = maxNormValue - middleNormValue; + float pLength = maxValue - middleValue; + if (pLength != 0f) + { + result = paramValue * (nLength / pLength); + result += middleNormValue; + } + + break; + } + case -1: + { + float nLength = minNormValue - middleNormValue; + float pLength = minValue - middleValue; + if (pLength != 0f) + { + result = paramValue * (nLength / pLength); + result += middleNormValue; + } + + break; + } + default: + { + result = middleNormValue; + + break; + } + } + + return isInverted + ? result + : result * -1f; + } + + /// SDK GetInputTranslationXFromNormalizedParameterValue. + internal static void GetInputTranslationXFromNormalizedParameterValue( + ref float targetTranslationX, ref float targetTranslationY, ref float targetAngle, + float value, float parameterMinimumValue, float parameterMaximumValue, float parameterDefaultValue, + CubismPhysicsNormalization normalizationPosition, CubismPhysicsNormalization normalizationAngle, + bool isInverted, float weight) + { + targetTranslationX += NormalizeParameterValue( + value, + parameterMinimumValue, + parameterMaximumValue, + parameterDefaultValue, + normalizationPosition.Minimum, + normalizationPosition.Maximum, + normalizationPosition.Default, + isInverted + ) * weight; + } + + /// SDK GetInputTranslationYFromNormalizedParameterValue. + internal static void GetInputTranslationYFromNormalizedParameterValue( + ref float targetTranslationX, ref float targetTranslationY, ref float targetAngle, + float value, float parameterMinimumValue, float parameterMaximumValue, float parameterDefaultValue, + CubismPhysicsNormalization normalizationPosition, CubismPhysicsNormalization normalizationAngle, + bool isInverted, float weight) + { + targetTranslationY += NormalizeParameterValue( + value, + parameterMinimumValue, + parameterMaximumValue, + parameterDefaultValue, + normalizationPosition.Minimum, + normalizationPosition.Maximum, + normalizationPosition.Default, + isInverted + ) * weight; + } + + /// SDK GetInputAngleFromNormalizedParameterValue. + internal static void GetInputAngleFromNormalizedParameterValue( + ref float targetTranslationX, ref float targetTranslationY, ref float targetAngle, + float value, float parameterMinimumValue, float parameterMaximumValue, float parameterDefaultValue, + CubismPhysicsNormalization normalizationPosition, CubismPhysicsNormalization normalizationAngle, + bool isInverted, float weight) + { + targetAngle += NormalizeParameterValue( + value, + parameterMinimumValue, + parameterMaximumValue, + parameterDefaultValue, + normalizationAngle.Minimum, + normalizationAngle.Maximum, + normalizationAngle.Default, + isInverted + ) * weight; + } + + /// + /// SDK UpdateParticles: one integration step of a particle strand. + /// The root is pinned to the total translation, each particle keeps its + /// radius distance from its predecessor, is rotated with the gravity + /// change (divided by air resistance), integrates velocity and force, + /// and stores velocity for the next step (only when delay != 0). + /// + internal static void UpdateParticles( + CubismPhysicsParticle[] strand, int baseParticleIndex, int strandCount, + float totalTranslationX, float totalTranslationY, float totalAngle, + float windX, float windY, float thresholdValue, float deltaTimeSeconds, float airResistance) + { + strand[baseParticleIndex].PositionX = totalTranslationX; + strand[baseParticleIndex].PositionY = totalTranslationY; + + float totalRadian = DegreesToRadian(totalAngle); + float currentGravityX = SinF(totalRadian); + float currentGravityY = CosF(totalRadian); + Normalize(ref currentGravityX, ref currentGravityY); + + for (int i = 1; i < strandCount; ++i) + { + int idx = baseParticleIndex + i; + + strand[idx].ForceX = currentGravityX * strand[idx].Acceleration + windX; + strand[idx].ForceY = currentGravityY * strand[idx].Acceleration + windY; + + strand[idx].LastPositionX = strand[idx].PositionX; + strand[idx].LastPositionY = strand[idx].PositionY; + + float delay = strand[idx].Delay * deltaTimeSeconds * 30f; + + float directionX = strand[idx].PositionX - strand[idx - 1].PositionX; + float directionY = strand[idx].PositionY - strand[idx - 1].PositionY; + + float radian = DirectionToRadian(strand[idx].LastGravityX, strand[idx].LastGravityY, + currentGravityX, currentGravityY) / airResistance; + + // NOTE: native computes Y from the already-updated X + // (bug-compatible with CubismPhysics.cpp UpdateParticles). + float newDirectionX = CosF(radian) * directionX - directionY * SinF(radian); + directionY = SinF(radian) * newDirectionX + directionY * CosF(radian); + directionX = newDirectionX; + + strand[idx].PositionX = strand[idx - 1].PositionX + directionX; + strand[idx].PositionY = strand[idx - 1].PositionY + directionY; + + float velocityX = strand[idx].VelocityX * delay; + float velocityY = strand[idx].VelocityY * delay; + float forceX = strand[idx].ForceX * delay * delay; + float forceY = strand[idx].ForceY * delay * delay; + + strand[idx].PositionX = strand[idx].PositionX + velocityX + forceX; + strand[idx].PositionY = strand[idx].PositionY + velocityY + forceY; + + float newDirectionX2 = strand[idx].PositionX - strand[idx - 1].PositionX; + float newDirectionY2 = strand[idx].PositionY - strand[idx - 1].PositionY; + Normalize(ref newDirectionX2, ref newDirectionY2); + + strand[idx].PositionX = strand[idx - 1].PositionX + newDirectionX2 * strand[idx].Radius; + strand[idx].PositionY = strand[idx - 1].PositionY + newDirectionY2 * strand[idx].Radius; + + if (AbsF(strand[idx].PositionX) < thresholdValue) + { + strand[idx].PositionX = 0f; + } + + if (delay != 0f) + { + strand[idx].VelocityX = strand[idx].PositionX - strand[idx].LastPositionX; + strand[idx].VelocityY = strand[idx].PositionY - strand[idx].LastPositionY; + strand[idx].VelocityX /= delay; + strand[idx].VelocityY /= delay; + strand[idx].VelocityX *= strand[idx].Mobility; + strand[idx].VelocityY *= strand[idx].Mobility; + } + + strand[idx].ForceX = 0f; + strand[idx].ForceY = 0f; + strand[idx].LastGravityX = currentGravityX; + strand[idx].LastGravityY = currentGravityY; + } + } + + /// + /// SDK UpdateParticlesForStabilization: velocity is zeroed and each + /// particle snaps to one normalized-force radius from its predecessor. + /// + internal static void UpdateParticlesForStabilization( + CubismPhysicsParticle[] strand, int baseParticleIndex, int strandCount, + float totalTranslationX, float totalTranslationY, float totalAngle, + float windX, float windY, float thresholdValue) + { + strand[baseParticleIndex].PositionX = totalTranslationX; + strand[baseParticleIndex].PositionY = totalTranslationY; + + float totalRadian = DegreesToRadian(totalAngle); + float currentGravityX = SinF(totalRadian); + float currentGravityY = CosF(totalRadian); + Normalize(ref currentGravityX, ref currentGravityY); + + for (int i = 1; i < strandCount; ++i) + { + int idx = baseParticleIndex + i; + + strand[idx].ForceX = currentGravityX * strand[idx].Acceleration + windX; + strand[idx].ForceY = currentGravityY * strand[idx].Acceleration + windY; + + strand[idx].LastPositionX = strand[idx].PositionX; + strand[idx].LastPositionY = strand[idx].PositionY; + + strand[idx].VelocityX = 0f; + strand[idx].VelocityY = 0f; + + float forceX = strand[idx].ForceX; + float forceY = strand[idx].ForceY; + Normalize(ref forceX, ref forceY); + + forceX *= strand[idx].Radius; + forceY *= strand[idx].Radius; + strand[idx].PositionX = strand[idx - 1].PositionX + forceX; + strand[idx].PositionY = strand[idx - 1].PositionY + forceY; + + if (AbsF(strand[idx].PositionX) < thresholdValue) + { + strand[idx].PositionX = 0f; + } + + strand[idx].ForceX = 0f; + strand[idx].ForceY = 0f; + strand[idx].LastGravityX = currentGravityX; + strand[idx].LastGravityY = currentGravityY; + } + } + + // ================================================================== + // SDK math equivalents (bit-exact formulas; do not "simplify"). + // ================================================================== + + /// SDK CubismMath::DegreesToRadian — note the divide-first order. + internal static float DegreesToRadian(float degrees) + { + return (degrees / 180f) * Pi; + } + + /// SDK CubismMath::DirectionToRadian — atan2 difference wrapped into [-Pi, Pi]. + internal static float DirectionToRadian(float fromX, float fromY, float toX, float toY) + { + float q1 = MathF.Atan2(toY, toX); + float q2 = MathF.Atan2(fromY, fromX); + + float ret = q1 - q2; + + while (ret < -Pi) + { + ret += Pi * 2f; + } + + while (ret > Pi) + { + ret -= Pi * 2f; + } + + return ret; + } + + /// SDK CubismMath::RadianToDirection — (sin, cos). + internal static void RadianToDirection(float totalAngle, out float x, out float y) + { + x = SinF(totalAngle); + y = CosF(totalAngle); + } + + /// + /// SDK CubismVector2::Normalize — note that native uses + /// powf(squareLength, 0.5f), not sqrtf, and does not guard zero length. + /// + internal static void Normalize(ref float x, ref float y) + { + float length = MathF.Pow(x * x + y * y, 0.5f); + + x = x / length; + y = y / length; + } + + private static float SinF(float x) => MathF.Sin(x); + + private static float CosF(float x) => MathF.Cos(x); + + private static float AbsF(float x) => MathF.Abs(x); + + private static float MinF(float l, float r) => (l > r) ? r : l; + + private static float MaxF(float l, float r) => (l > r) ? l : r; + + /// Resolves a parameter id to its index; -1 when not present (native asserts instead). + private static unsafe int ResolveParameterIndex(CubismNativeModel model, string parameterId) { 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) + for (int i = 0; i < model.ParameterCount; i++) { - float totalTranslationX = 0f, totalTranslationY = 0f, totalAngle = 0f; - - for (int ii = 0; ii < subRig.InputCount; ii++) + if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, i) == parameterId) { - 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; + return i; } } + + return -1; } - private float EvaluateOutputValue(int outputIndex) + /// Maps a physics3.json type tag to its source enum; unknown tags behave as X (enum value 0, like native). + private static CubismPhysicsSource ParseSource(string value) { - 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 + switch (value) { - 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; + case "Angle": return CubismPhysicsSource.Angle; + case "Y": return CubismPhysicsSource.Y; + default: return CubismPhysicsSource.X; + } } } } diff --git a/source_code/Cubism/Framework/CubismPhysicsJson.cs b/source_code/Cubism/Framework/CubismPhysicsJson.cs index f5220a1..6bf4566 100644 --- a/source_code/Cubism/Framework/CubismPhysicsJson.cs +++ b/source_code/Cubism/Framework/CubismPhysicsJson.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Text.Json; @@ -17,7 +16,7 @@ namespace Cthangover.Live2D.Cubism.Framework /// /// 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. + /// .physics3.json file. Mirrors SDK CubismPhysicsParticle. /// public struct CubismPhysicsParticle { @@ -61,8 +60,11 @@ namespace Cthangover.Live2D.Cubism.Framework /// /// 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. + /// is the pendulum vertex index relative to the + /// owning sub-rig's base particle (SDK semantics). The translation scale is + /// deliberately never loaded: native CubismPhysicsJson reads only the + /// single "Scale" value into , so X/Y-typed outputs + /// scale by zero exactly like the native implementation. /// public struct CubismPhysicsOutput { @@ -95,146 +97,251 @@ namespace Cthangover.Live2D.Cubism.Framework } /// - /// 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. + /// JSON parser for the Cubism .physics3.json format. Field-by-field port of + /// Cubism SDK for Native 5-r.5 Physics/CubismPhysicsJson.cpp: every getter + /// reads the same key at the same JSON path. Missing numeric values yield + /// 0 / false like the native error-value accessors; in particular a + /// missing Meta.Fps yields 0 (the simulation then steps per display frame). + /// Note: native does not read InitialPosition/InitialVelocity for vertices — + /// those keys are ignored here as well; particles are laid out by + /// CubismPhysics.Initialize(). /// public class CubismPhysicsJson { private readonly JsonDocument _json; - /// - /// Parses the given .physics3.json bytes. - /// + /// 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; - } + // ------------------------------------------------------------------ + // Meta + // ------------------------------------------------------------------ - /// 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). + /// Gravity vector from Meta.EffectiveForces.Gravity (missing values read as 0). 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 = 0f; + y = 0f; + if (_json.RootElement.TryGetProperty("Meta", out var meta) && + meta.TryGetProperty("EffectiveForces", out var effectiveForces) && + effectiveForces.TryGetProperty("Gravity", out var gravity)) { - x = g.TryGetProperty("X", out var gx) ? gx.GetSingle() : 0f; - y = g.TryGetProperty("Y", out var gy) ? gy.GetSingle() : -1f; + x = GetFloat(gravity, "X"); + y = GetFloat(gravity, "Y"); } } - /// Reads wind vector from Meta.EffectiveForces.Wind. Defaults to (0, 0). + /// Wind vector from Meta.EffectiveForces.Wind (missing values read as 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 = 0f; + y = 0f; + if (_json.RootElement.TryGetProperty("Meta", out var meta) && + meta.TryGetProperty("EffectiveForces", out var effectiveForces) && + effectiveForces.TryGetProperty("Wind", out var wind)) { - x = w.TryGetProperty("X", out var wx) ? wx.GetSingle() : 0f; - y = w.TryGetProperty("Y", out var wy) ? wy.GetSingle() : 0f; + x = GetFloat(wind, "X"); + y = GetFloat(wind, "Y"); } } - /// Number of input entries in the given sub-rig. - public int GetInputCount(int subRigIndex) => - GetSubRig(subRigIndex).GetProperty("Input").GetArrayLength(); + /// Simulation FPS from Meta.Fps; 0 when absent (native behavior). + public float GetFps() + { + return _json.RootElement.TryGetProperty("Meta", out var meta) ? GetFloat(meta, "Fps") : 0f; + } - /// Number of output entries in the given sub-rig. - public int GetOutputCount(int subRigIndex) => - GetSubRig(subRigIndex).GetProperty("Output").GetArrayLength(); + /// Sub-rig count from Meta.PhysicsSettingCount. + public int GetSubRigCount() + { + if (_json.RootElement.TryGetProperty("Meta", out var meta) && + meta.TryGetProperty("PhysicsSettingCount", out var count)) + { + return count.GetInt32(); + } - /// Number of vertex/particle entries in the given sub-rig. - public int GetVertexCount(int subRigIndex) => - GetSubRig(subRigIndex).GetProperty("Vertices").GetArrayLength(); + // Defensive fallback: use the actual settings array length + // (native would return 0 when the key is absent). + return _json.RootElement.TryGetProperty("PhysicsSettings", out var settings) + ? settings.GetArrayLength() + : 0; + } - /// Reads position normalization (min, max, default) for the given sub-rig. + /// Total input count from Meta.TotalInputCount. + public int GetTotalInputCount() => GetMetaInt("TotalInputCount"); + + /// Total output count from Meta.TotalOutputCount. + public int GetTotalOutputCount() => GetMetaInt("TotalOutputCount"); + + /// Total vertex/particle count from Meta.VertexCount. + public int GetVertexCount() => GetMetaInt("VertexCount"); + + // ------------------------------------------------------------------ + // Normalization + // ------------------------------------------------------------------ + + /// Position normalization (Minimum/Maximum/Default) for a sub-rig; missing values read as 0. 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; + if (GetSubRig(subRigIndex).TryGetProperty("Normalization", out var n) && + n.TryGetProperty("Position", out var pos)) + { + norm.Minimum = GetFloat(pos, "Minimum"); + norm.Maximum = GetFloat(pos, "Maximum"); + norm.Default = GetFloat(pos, "Default"); + } } - /// Reads angle normalization (min, max, default) for the given sub-rig. + /// Angle normalization (Minimum/Maximum/Default) for a sub-rig; missing values read as 0. 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; + if (GetSubRig(subRigIndex).TryGetProperty("Normalization", out var n) && + n.TryGetProperty("Angle", out var angle)) + { + norm.Minimum = GetFloat(angle, "Minimum"); + norm.Maximum = GetFloat(angle, "Maximum"); + norm.Default = GetFloat(angle, "Default"); + } } - /// 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) + // ------------------------------------------------------------------ + // Input + // ------------------------------------------------------------------ + + /// Number of input entries in the given sub-rig. + public int GetInputCount(int subRigIndex) => GetEntriesLength(subRigIndex, "Input"); + + /// Input weight (Input[i].Weight). + public float GetInputWeight(int subRigIndex, int inputIndex) => + GetFloat(GetInputEntry(subRigIndex, inputIndex), "Weight"); + + /// Input reflect flag (Input[i].Reflect). + public bool GetInputReflect(int subRigIndex, int inputIndex) => + GetBool(GetInputEntry(subRigIndex, inputIndex), "Reflect"); + + /// Input type tag (Input[i].Type): "X", "Y" or "Angle". + public string GetInputType(int subRigIndex, int inputIndex) => + GetString(GetInputEntry(subRigIndex, inputIndex), "Type"); + + /// Source parameter id (Input[i].Source.Id). + public string GetInputSourceId(int subRigIndex, int inputIndex) { - 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(); + var entry = GetInputEntry(subRigIndex, inputIndex); + return entry.TryGetProperty("Source", out var source) ? GetString(source, "Id") : null; } - /// 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) + // ------------------------------------------------------------------ + // Output + // ------------------------------------------------------------------ + + /// Number of output entries in the given sub-rig. + public int GetOutputCount(int subRigIndex) => GetEntriesLength(subRigIndex, "Output"); + + /// Pendulum vertex index (Output[i].VertexIndex), relative to the sub-rig base particle. + public int GetOutputVertexIndex(int subRigIndex, int outputIndex) => + GetInt(GetOutputEntry(subRigIndex, outputIndex), "VertexIndex"); + + /// Angle scale (Output[i].Scale). Native maps the single "Scale" value to the angle scale only. + public float GetOutputAngleScale(int subRigIndex, int outputIndex) => + GetFloat(GetOutputEntry(subRigIndex, outputIndex), "Scale"); + + /// Output weight (Output[i].Weight). + public float GetOutputWeight(int subRigIndex, int outputIndex) => + GetFloat(GetOutputEntry(subRigIndex, outputIndex), "Weight"); + + /// Destination parameter id (Output[i].Destination.Id). + public string GetOutputsDestinationId(int subRigIndex, int outputIndex) { - 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(); + var entry = GetOutputEntry(subRigIndex, outputIndex); + return entry.TryGetProperty("Destination", out var destination) ? GetString(destination, "Id") : null; } - /// 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) + /// Output type tag (Output[i].Type): "X", "Y" or "Angle". + public string GetOutputType(int subRigIndex, int outputIndex) => + GetString(GetOutputEntry(subRigIndex, outputIndex), "Type"); + + /// Output reflect flag (Output[i].Reflect). + public bool GetOutputReflect(int subRigIndex, int outputIndex) => + GetBool(GetOutputEntry(subRigIndex, outputIndex), "Reflect"); + + // ------------------------------------------------------------------ + // Particle (Vertices) + // ------------------------------------------------------------------ + + /// Number of vertex/particle entries in the given sub-rig. + public int GetParticleCount(int subRigIndex) => GetEntriesLength(subRigIndex, "Vertices"); + + /// Particle mobility (Vertices[i].Mobility). + public float GetParticleMobility(int subRigIndex, int vertexIndex) => + GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Mobility"); + + /// Particle delay (Vertices[i].Delay). + public float GetParticleDelay(int subRigIndex, int vertexIndex) => + GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Delay"); + + /// Particle acceleration (Vertices[i].Acceleration). + public float GetParticleAcceleration(int subRigIndex, int vertexIndex) => + GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Acceleration"); + + /// Particle radius (Vertices[i].Radius). + public float GetParticleRadius(int subRigIndex, int vertexIndex) => + GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Radius"); + + /// Particle position (Vertices[i].Position.X/Y). + public void GetParticlePosition(int subRigIndex, int vertexIndex, out float x, out float y) { - 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; + var entry = GetVertexEntry(subRigIndex, vertexIndex); + x = 0f; + y = 0f; + if (entry.TryGetProperty("Position", out var position)) + { + x = GetFloat(position, "X"); + y = GetFloat(position, "Y"); + } } + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + private JsonElement GetSubRig(int index) => _json.RootElement.GetProperty("PhysicsSettings")[index]; - private static CubismPhysicsSource ParseSource(string v) => v switch + private JsonElement GetInputEntry(int subRigIndex, int inputIndex) => + GetSubRig(subRigIndex).GetProperty("Input")[inputIndex]; + + private JsonElement GetOutputEntry(int subRigIndex, int outputIndex) => + GetSubRig(subRigIndex).GetProperty("Output")[outputIndex]; + + private JsonElement GetVertexEntry(int subRigIndex, int vertexIndex) => + GetSubRig(subRigIndex).GetProperty("Vertices")[vertexIndex]; + + private int GetEntriesLength(int subRigIndex, string name) { - "Angle" => CubismPhysicsSource.Angle, - "Y" => CubismPhysicsSource.Y, - _ => CubismPhysicsSource.X - }; + var subRig = GetSubRig(subRigIndex); + return subRig.TryGetProperty(name, out var entries) ? entries.GetArrayLength() : 0; + } + + private int GetMetaInt(string name) => + _json.RootElement.TryGetProperty("Meta", out var meta) ? GetInt(meta, name) : 0; + + private static float GetFloat(JsonElement element, string name) => + element.TryGetProperty(name, out var value) ? value.GetSingle() : 0f; + + private static int GetInt(JsonElement element, string name) => + element.TryGetProperty(name, out var value) ? value.GetInt32() : 0; + + private static bool GetBool(JsonElement element, string name) => + element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.True; + + private static string GetString(JsonElement element, string name) => + element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String + ? value.GetString() + : string.Empty; } } diff --git a/source_code/Cubism/Framework/CubismPose.cs b/source_code/Cubism/Framework/CubismPose.cs index ed003bc..cd0aed3 100644 --- a/source_code/Cubism/Framework/CubismPose.cs +++ b/source_code/Cubism/Framework/CubismPose.cs @@ -6,44 +6,75 @@ 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: + /// Pose groups organize model parts into mutual-exclusion groups — only one + /// part in each group is visible at a time. Commonly used for eye states + /// (open/closed/surprised) and other multi-state part switches. + /// + /// This is a faithful port of CubismPose.cpp (Cubism SDK for Native 5-r.5): /// - /// 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 + /// Part groups are stored as a flattened list with per-group counts + /// (matching the SDK layout, so index arithmetic in Reset/DoFade is identical). + /// Each group part references both a part index (for opacity) and a + /// parameter index (for visibility). The visible part of a group is + /// decided by the PARAMETER VALUE (> Epsilon), not by the part opacity. + /// If a pose parameter/part does not exist in the model, the SDK's + /// "not exist" maps are emulated, and the group falls back to its + /// first part with opacity 1. + /// Reset (on model change): the first part of each group gets + /// parameter value 1 / part opacity 1, all others get 0; linked parts + /// resolve their indices too. + /// DoFade: the visible part fades toward 1 at rate + /// deltaTime / fadeTimeSeconds while all other parts are pushed toward + /// 0 with the SDK's piecewise-linear crossing formula (Phi = 0.5) and + /// back-opacity threshold (0.15). If no part is visible, the group + /// falls back to its first part with opacity 1. If fadeTimeSeconds is + /// 0, the visible part becomes fully opaque immediately. + /// CopyPartOpacities: parts with a "Link" list copy their current + /// part opacity to every linked part. /// - /// - /// 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 + private const float Epsilon = 0.001f; + private const float DefaultFadeInSeconds = 0.5f; + private const float Phi = 0.5f; + private const float BackOpacityThreshold = 0.15f; + + private sealed class PartData { public string PartId; - public int PartIndex; - public int ParameterIndex; - public List Link; + public int PartIndex = -1; + public int ParameterIndex = -1; + public readonly List Link = new(); + + /// + /// SDK CubismPose::PartData::Initialize — resolves both indices for + /// this part id and initializes its pose parameter to 1. + /// + public void Initialize(CubismNativeModel model, CubismPose owner) + { + ParameterIndex = owner.FindParameterIndex(model, PartId); + PartIndex = owner.FindPartIndex(model, PartId); + owner.SetParameterValue(model, ParameterIndex, PartId, 1f); + } } - private readonly List> _partGroups = new(); - private readonly float _fadeTimeSeconds; + private readonly List _partGroups = new(); + private readonly List _partGroupCounts = new(); + private float _fadeTimeSeconds = DefaultFadeInSeconds; private IntPtr _lastModel = IntPtr.Zero; + // Emulation of the SDK's _notExistParameterValues / _notExistPartOpacities maps: + // pose3.json may reference parameters/parts that do not exist in the model. + // They are keyed by id here (the SDK keys parameter entries by synthesized + // indices; per-id keys are equivalent for this emulation). + private readonly Dictionary _notExistParameterValues = new(); + private readonly Dictionary _notExistPartOpacities = new(); + /// /// Parses a .pose3.json byte buffer. - /// Extracts the global fade time and all part groups with their - /// linked-part chains. + /// Extracts the fade time and all part groups (flattened, with counts per + /// group) including their linked-part chains. /// public CubismPose(byte[] pose3JsonBytes) { @@ -51,168 +82,273 @@ namespace Cthangover.Live2D.Cubism.Framework 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; + if (root.TryGetProperty("FadeInTime", out var ft) && ft.ValueKind == JsonValueKind.Number) + { + _fadeTimeSeconds = ft.GetSingle(); + if (_fadeTimeSeconds < 0f) + _fadeTimeSeconds = DefaultFadeInSeconds; + } + + if (!root.TryGetProperty("Groups", out var groups) || groups.ValueKind != JsonValueKind.Array) return; foreach (var group in groups.EnumerateArray()) { - var partList = new List(); + var groupCount = 0; foreach (var part in group.EnumerateArray()) { var pd = new PartData { - PartId = part.GetProperty("Id").GetString(), - PartIndex = -1, - ParameterIndex = -1, - Link = new List() + PartId = part.GetProperty("Id").GetString() }; - if (part.TryGetProperty("Link", out var links)) + if (part.TryGetProperty("Link", out var links) && links.ValueKind == JsonValueKind.Array) { foreach (var link in links.EnumerateArray()) - pd.Link.Add(new PartData { PartId = link.GetString(), PartIndex = -1, ParameterIndex = -1 }); + pd.Link.Add(new PartData { PartId = link.GetString() }); } - partList.Add(pd); + _partGroups.Add(pd); + ++groupCount; } - _partGroups.Add(partList); + _partGroupCounts.Add(groupCount); } } /// - /// 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. + /// Per-frame pose update, mirroring CubismPose::UpdateParameters. + /// Detects model changes to trigger a full reset, then fades each group + /// and propagates opacities to linked parts. /// Called from after expressions. /// - public unsafe void UpdateParameters(CubismNativeModel model, float deltaTimeSeconds) + public 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); + _lastModel = model.ModelPtr; + + // 設定から時間を変更すると、経過時間がマイナスになることがあるので、経過時間0として対応。 + if (deltaTimeSeconds < 0f) + deltaTimeSeconds = 0f; + + var beginIndex = 0; + for (int i = 0; i < _partGroupCounts.Count; i++) + { + var partGroupCount = _partGroupCounts[i]; + DoFade(model, deltaTimeSeconds, beginIndex, partGroupCount); + beginIndex += partGroupCount; + } CopyPartOpacities(model); } + /// SDK CubismPose::Reset — re-resolves indices and restores the + /// default state: the first part of each group is visible (parameter 1, + /// opacity 1), all others hidden, links re-initialized. private unsafe void Reset(CubismNativeModel model) { - var idsPtr = model.GetPartIds(); - var opacities = model.GetPartOpacities(); - int partCount = model.GetPartCount(); + var beginIndex = 0; - foreach (var group in _partGroups) + for (int gi = 0; gi < _partGroupCounts.Count; gi++) { - for (int i = 0; i < group.Count; i++) + var groupCount = _partGroupCounts[gi]; + + for (int j = beginIndex; j < beginIndex + groupCount; j++) { - var pd = group[i]; - pd.PartIndex = -1; - pd.ParameterIndex = -1; + var pd = _partGroups[j]; + pd.Initialize(model, this); - for (int pi = 0; pi < partCount; pi++) - { - if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == pd.PartId) - { - pd.PartIndex = pi; - opacities[pi] = i == 0 ? 1f : 0f; - break; - } - } + if (pd.PartIndex < 0) + continue; - group[i] = pd; + SetPartOpacity(model, pd.PartIndex, pd.PartId, j == beginIndex ? 1f : 0f); + SetParameterValue(model, pd.ParameterIndex, pd.PartId, j == beginIndex ? 1f : 0f); + + for (int k = 0; k < pd.Link.Count; k++) + pd.Link[k].Initialize(model, this); } + + beginIndex += groupCount; } } - private unsafe void DoFade(CubismNativeModel model, List group, float deltaTimeSeconds) + /// SDK CubismPose::DoFade — visibility is driven by the group + /// part PARAMETER values; the visible part fades in while the rest are + /// faded out with the crossing-threshold formula. + private unsafe void DoFade(CubismNativeModel model, float deltaTimeSeconds, int beginIndex, int partGroupCount) { - const float Phi = 0.5f; - const float BackOpacityThreshold = 0.15f; + var visiblePartIndex = -1; + var newOpacity = 1f; - 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++) + // 現在、表示状態になっているパーツを取得 + for (int i = beginIndex; i < beginIndex + partGroupCount; i++) { - var pd = group[i]; - if (pd.PartIndex < 0) continue; + var pd = _partGroups[i]; - if (opacities[pd.PartIndex] > 0.001f) + if (GetParameterValue(model, pd.ParameterIndex, pd.PartId) > Epsilon) { - visibleIndex = i; - break; + if (visiblePartIndex >= 0) + break; + + visiblePartIndex = i; + + if (_fadeTimeSeconds == 0f) + { + newOpacity = 1f; + continue; + } + + newOpacity = ComputeVisibleOpacity( + GetPartOpacity(model, pd.PartIndex, pd.PartId), + deltaTimeSeconds, + _fadeTimeSeconds); } } - for (int i = 0; i < group.Count; i++) + if (visiblePartIndex < 0) { - var pd = group[i]; - if (pd.PartIndex < 0) continue; + visiblePartIndex = 0; + newOpacity = 1f; + } - if (i == visibleIndex) + // 表示パーツ、非表示パーツの不透明度を設定する + for (int i = beginIndex; i < beginIndex + partGroupCount; i++) + { + var pd = _partGroups[i]; + + if (visiblePartIndex == i) { - var fadeRate = deltaTimeSeconds / _fadeTimeSeconds; - newOpacity = MathF.Min(opacities[pd.PartIndex] + fadeRate, 1f); - opacities[pd.PartIndex] = newOpacity; + SetPartOpacity(model, pd.PartIndex, pd.PartId, newOpacity); // 先に設定 } else { - float a1; - if (newOpacity < Phi) - a1 = newOpacity * (Phi - 1f) / Phi + 1f; - else - a1 = (1f - newOpacity) * Phi / (1f - Phi); + var opacity = GetPartOpacity(model, pd.PartIndex, pd.PartId); + var a1 = ComputeHiddenOpacity(newOpacity); - var backOpacity = (1f - a1) * (1f - newOpacity); - if (backOpacity > BackOpacityThreshold) - a1 = 1f - BackOpacityThreshold / (1f - newOpacity); + if (opacity > a1) + opacity = a1; - opacities[pd.PartIndex] = MathF.Min(opacities[pd.PartIndex], a1); + SetPartOpacity(model, pd.PartIndex, pd.PartId, opacity); } } } + /// SDK CubismPose::CopyPartOpacities — every group part with a + /// Link list copies its current part opacity to all linked parts. private unsafe void CopyPartOpacities(CubismNativeModel model) { - var idsPtr = model.GetPartIds(); - var opacities = model.GetPartOpacities(); - int partCount = model.GetPartCount(); - - foreach (var group in _partGroups) + foreach (var partData in _partGroups) { - for (int gi = 0; gi < group.Count; gi++) - { - var pd = group[gi]; - if (pd.PartIndex < 0 || pd.Link == null) continue; + if (partData.Link.Count == 0) + 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; - } - } - } + var opacity = GetPartOpacity(model, partData.PartIndex, partData.PartId); + + for (int linkIndex = 0; linkIndex < partData.Link.Count; linkIndex++) + { + var linkPart = partData.Link[linkIndex]; + + if (linkPart.PartIndex < 0) + continue; + + SetPartOpacity(model, linkPart.PartIndex, linkPart.PartId, opacity); } } } + + /// Resolves a part id to a part index, or -1 when absent. + private unsafe int FindPartIndex(CubismNativeModel model, string partId) + { + var idsPtr = model.GetPartIds(); + var partCount = model.GetPartCount(); + for (int i = 0; i < partCount; i++) + { + if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, i) == partId) + return i; + } + return -1; + } + + /// Resolves a parameter id to a parameter index, or -1 when absent. + private unsafe int FindParameterIndex(CubismNativeModel model, string parameterId) + { + var idsPtr = model.GetParameterIds(); + var parameterCount = model.ParameterCount; + for (int i = 0; i < parameterCount; i++) + { + if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, i) == parameterId) + return i; + } + return -1; + } + + private unsafe float GetParameterValue(CubismNativeModel model, int parameterIndex, string parameterId) + { + if (parameterIndex < 0) + return _notExistParameterValues.TryGetValue(parameterId, out var v) ? v : 0f; + return model.GetParameterValues()[parameterIndex]; + } + + private unsafe void SetParameterValue(CubismNativeModel model, int parameterIndex, string parameterId, float value) + { + if (parameterIndex < 0) + { + _notExistParameterValues[parameterId] = value; + return; + } + model.GetParameterValues()[parameterIndex] = value; + } + + private unsafe float GetPartOpacity(CubismNativeModel model, int partIndex, string partId) + { + if (partIndex < 0) + return _notExistPartOpacities.TryGetValue(partId, out var v) ? v : 0f; + return model.GetPartOpacities()[partIndex]; + } + + private unsafe void SetPartOpacity(CubismNativeModel model, int partIndex, string partId, float opacity) + { + if (partIndex < 0) + { + _notExistPartOpacities[partId] = opacity; + return; + } + model.GetPartOpacities()[partIndex] = opacity; + } + + /// + /// Pure helper (SDK DoFade, visible branch): the visible part's opacity + /// grows by deltaTimeSeconds / fadeTimeSeconds, clamped to 1. + /// Internal for unit tests. + /// + internal static float ComputeVisibleOpacity(float currentOpacity, float deltaTimeSeconds, float fadeTimeSeconds) + { + var newOpacity = currentOpacity + (deltaTimeSeconds / fadeTimeSeconds); + return newOpacity > 1f ? 1f : newOpacity; + } + + /// + /// Pure helper (SDK DoFade, hidden branch): piecewise-linear alpha with + /// the Phi = 0.5 crossing and the 0.15 back-opacity threshold. + /// Internal for unit tests. + /// + internal static float ComputeHiddenOpacity(float newOpacity) + { + float a1; + if (newOpacity < Phi) + a1 = newOpacity * (Phi - 1f) / Phi + 1f; // (0,1),(phi,phi)を通る直線式 + else + a1 = (1f - newOpacity) * Phi / (1f - Phi); // (1,0),(phi,phi)を通る直線式 + + var backOpacity = (1f - a1) * (1f - newOpacity); + if (backOpacity > BackOpacityThreshold) + a1 = 1f - BackOpacityThreshold / (1f - newOpacity); + + return a1; + } } } diff --git a/source_code/Cubism/Framework/CubismRenderer2D.cs b/source_code/Cubism/Framework/CubismRenderer2D.cs index 45d0d10..9d0470b 100644 --- a/source_code/Cubism/Framework/CubismRenderer2D.cs +++ b/source_code/Cubism/Framework/CubismRenderer2D.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using System.Collections.Generic; using Cthangover.Core.Utils; using Godot; +using Cthangover.Core.Mods.Resolvers; using Cthangover.Live2D.Cubism.Core; namespace Cthangover.Live2D.Cubism.Framework @@ -187,9 +188,18 @@ namespace Cthangover.Live2D.Cubism.Framework GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] opacity={opacity:F3}"); var shader = ResolveShaderForDrawable(di, constantFlags, maskCounts); + if (shader == null) + { + // Shader failed to resolve — skip this drawable instead of + // building a material-less mesh (guards downstream NREs and + // keeps the rest of the model rendering). + GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] SKIPPED — shader not resolved", LogLevel.Error); + _meshInstances[di] = null; + _materials[di] = null; + continue; + } - ShaderMaterial mat = null; - if (shader != null) + ShaderMaterial mat; { mat = new ShaderMaterial { Shader = shader }; mat.SetShaderParameter("color_base", new Color(1f, 1f, 1f, 1f)); @@ -221,15 +231,8 @@ namespace Cthangover.Live2D.Cubism.Framework 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})"); - } + if (texture == null) + GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] texture MISSING (texIndex={texIndex})", LogLevel.Warning); mi.Visible = true; _parentNode.AddChild(mi); @@ -428,6 +431,8 @@ namespace Cthangover.Live2D.Cubism.Framework var maskTexIndex = model.GetTextureIndices()[drawableIndex]; var texture = maskTexIndex >= 0 && maskTexIndex < model.Textures.Length ? model.Textures[maskTexIndex] : null; + mi2d.Mesh = mesh; + var shader = ResolveShader("2d_cubism_mask"); if (shader == null) return mesh; @@ -436,7 +441,6 @@ namespace Cthangover.Live2D.Cubism.Framework if (texture != null) mat.SetShaderParameter("tex_main", texture); - mi2d.Mesh = mesh; mi2d.Material = mat; return mesh; } @@ -478,14 +482,17 @@ namespace Cthangover.Live2D.Cubism.Framework mi.ZIndex = renderOrders[di]; - var mc = multiplyColors[di]; - mat.SetShaderParameter("color_multiply", new Color(mc.X, mc.Y, mc.Z, mc.W)); + if (mat != null) + { + 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)); + 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.OpacityDidChange) != 0) + mat.SetShaderParameter("color_base", new Color(1f, 1f, 1f, opacities[di])); + } if ((flags & CubismCoreBindings.VertexPositionsDidChange) != 0) { @@ -493,21 +500,103 @@ namespace Cthangover.Live2D.Cubism.Framework } } + UpdateMaskGroups(dynamicFlags, dc); + + _model.ResetDynamicFlags(); + } + + /// + /// Per-frame mask refresh, mirroring CubismRenderer::DrawClip: + /// the clip bounding box is recomputed from the CURRENT vertex positions + /// of the mask drawables each frame. When the clip rect moves, the mask + /// meshes are re-offset into the SubViewport space and the masked + /// drawables' mesh_offset shader parameter is refreshed; when the rect + /// outgrows the render target, the SubViewport is resized (power of two, + /// so this stays rare). Without flag changes and offset movement the + /// per-drawable work is skipped. + /// + private unsafe void UpdateMaskGroups(byte* dynamicFlags, int dc) + { + if (_maskGroups.Count == 0) return; + + var vertexCounts = _model.GetVertexCounts(); + var vertexPositions = _model.GetVertexPositions(); + var ppu = _model.PixelsPerUnit; + 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 maskIndices = group.MaskDrawableIndices; + if (maskIndices == null || maskIndices.Length == 0) continue; + + // Current bounds over the mask drawables (pixel space). + float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue; + var any = false; + for (int mi = 0; mi < maskIndices.Length; mi++) { - var di = group.MaskDrawableIndices[mi]; + var di = maskIndices[mi]; if (di < 0 || di >= dc) continue; - var mFlags = dynamicFlags[di]; - if ((mFlags & CubismCoreBindings.VertexPositionsDidChange) == 0) continue; + var vCount = vertexCounts[di]; + var vp = vertexPositions[di]; + for (int v = 0; v < vCount; v++) + { + float x = vp[v].X * ppu; + float y = -vp[v].Y * ppu; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + any = true; + } + } + if (!any) continue; + + const float pad = 4f; + float newOffsetX = minX - pad; + float newOffsetY = minY - pad; + float needed = Math.Max(maxX - minX + pad * 2f, maxY - minY + pad * 2f); + needed = Math.Max(needed, 64f); + int neededSize = NextPowerOfTwo((int)Mathf.Ceil(needed)); + + bool offsetChanged = newOffsetX != group.OffsetX || newOffsetY != group.OffsetY; + bool sizeChanged = neededSize != group.ViewportSize; + if (sizeChanged) + { + group.ViewportSize = neededSize; + group.Viewport.Size = new Vector2I(neededSize, neededSize); + } + if (offsetChanged) + { + group.OffsetX = newOffsetX; + group.OffsetY = newOffsetY; + } + _maskGroups[gi] = group; + + // Rewrite mask mesh vertices when the mask drawable moved or the + // clip rect moved (offsets are baked into the vertex positions). + for (int mi = 0; mi < maskIndices.Length; mi++) + { + var di = maskIndices[mi]; + if (di < 0 || di >= dc) continue; + bool flagged = (dynamicFlags[di] & CubismCoreBindings.VertexPositionsDidChange) != 0; + if (!flagged && !offsetChanged) continue; UpdateMaskMeshVertices(di, group.MaskArrayMeshes[mi], gi); } - } - _model.ResetDynamicFlags(); + // Masked drawables sample the mask texture through mesh_offset; + // refresh it whenever the clip rect moved or was resized. + if (offsetChanged || sizeChanged) + { + for (int di = 0; di < dc; di++) + { + if (_drawableMaskGroupIndex == null || di >= _drawableMaskGroupIndex.Length) break; + if (_drawableMaskGroupIndex[di] != gi) continue; + var mat = _materials[di]; + if (mat != null) + mat.SetShaderParameter("mesh_offset", new Vector2(group.OffsetX, group.OffsetY)); + } + } + } } private unsafe void UpdateMeshVertices(int drawableIndex, ArrayMesh mesh) @@ -594,7 +683,7 @@ namespace Cthangover.Live2D.Cubism.Framework _drawableMaskChannel = null; foreach (var group in _maskGroups) - group.Viewport.QueueFree(); + group.Viewport?.QueueFree(); _maskGroups.Clear(); } @@ -631,7 +720,9 @@ namespace Cthangover.Live2D.Cubism.Framework { try { - var shader = Cthangover.Core.Mods.ModManager.Instance?.ResolveShader(name); + // Canonical resource path: shader resolution goes through the mod + // resolver system (ShaderResolver via ModManager.GetResolver). + var shader = Cthangover.Core.Mods.ModManager.Instance?.GetResolver()?.Resolve(name); if (shader != null) { GameLogger.Log("LIVE2D", $"ResolveShader: '{name}' OK"); diff --git a/source_code/Cubism/Framework/CubismTargetPoint.cs b/source_code/Cubism/Framework/CubismTargetPoint.cs index a2c7976..7a0e4cd 100644 --- a/source_code/Cubism/Framework/CubismTargetPoint.cs +++ b/source_code/Cubism/Framework/CubismTargetPoint.cs @@ -6,21 +6,20 @@ 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). + /// + /// Bit-exact port of Cubism SDK for Native 5-r.5 Math/CubismTargetPoint.cpp: + /// a velocity-based follower that accelerates toward the target with a + /// capped acceleration (reaching top speed in 0.15 s), then smoothly + /// decelerates so the configured acceleration can still brake before the + /// target. All time is expressed in internal 30 fps frame units. /// public class CubismTargetPoint { - private const float FaceParamMaxV = 40f; - private const float FrameToMaxSpeed = 4.5f; + /// Internal reference frame rate (SDK FrameRate). + private const int FrameRate = 30; + + /// Threshold below which no movement is applied (SDK Epsilon). + private const float Epsilon = 0.01f; private float _faceTargetX, _faceTargetY; private float _faceX, _faceY; @@ -45,54 +44,92 @@ namespace Cthangover.Live2D.Cubism.Framework } /// - /// 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. + /// Advances the smoothing simulation by one frame (SDK CubismTargetPoint::Update). + /// The very first call only anchors the internal clock and performs no + /// integration, matching the native implementation. /// public void Update(float deltaTimeSeconds) { + // Add the delta time. _userTimeSeconds += deltaTimeSeconds; - var deltaTimeWeight = (_userTimeSeconds - _lastTimeSeconds) * 30f; + + // The average speed of swinging the head from center to side is about 1 second. + // Considering acceleration/deceleration, twice that is the maximum speed. + // The face direction is 0.0 at center and +-1.0 at the sides. + const float FaceParamMaxV = 40f / 10f; // Moves 40 in 7.5 seconds (5.3/sc) + float MaxV = FaceParamMaxV * 1f / FrameRate; // Upper speed limit per frame + + // First call only anchors the clock; no integration (SDK behavior). + if (_lastTimeSeconds == 0f) + { + _lastTimeSeconds = _userTimeSeconds; + return; + } + + float deltaTimeWeight = (_userTimeSeconds - _lastTimeSeconds) * FrameRate; _lastTimeSeconds = _userTimeSeconds; - if (deltaTimeWeight <= 0f) return; + // Time until maximum speed, expressed in internal frames. + const float TimeToMaxSpeed = 0.15f; + const float FrameToMaxSpeed = TimeToMaxSpeed * FrameRate; // sec * frame/sec + float MaxA = deltaTimeWeight * MaxV / FrameToMaxSpeed; // Acceleration per frame - var maxV = FaceParamMaxV / 30f; - var maxA = deltaTimeWeight * maxV / FrameToMaxSpeed; + // The target direction is the vector (dx, dy). + float dx = _faceTargetX - _faceX; + float dy = _faceTargetY - _faceY; - 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) + if (MathF.Abs(dx) <= Epsilon && MathF.Abs(dy) <= Epsilon) { - var clampedA = MathF.Min(aLen, maxA); - ax = ax * clampedA / aLen; - ay = ay * clampedA / aLen; + return; // No change. } - 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) + // Slow down when the distance requires more than the maximum speed. + float d = MathF.Sqrt((dx * dx) + (dy * dy)); + + // Maximum speed vector toward the target. + float vx = MaxV * dx / d; + float vy = MaxV * dy / d; + + // Acceleration needed to change from the current velocity to the new one. + float ax = vx - _faceVX; + float ay = vy - _faceVY; + + float a = MathF.Sqrt((ax * ax) + (ay * ay)); + + // While accelerating. + if (a < -MaxA || a > MaxA) { - var scale = maxSafeV / vLen; - _faceVX *= scale; - _faceVY *= scale; + ax *= MaxA / a; + ay *= MaxA / a; } + // Add the acceleration to the original velocity to get the new velocity. _faceVX += ax; _faceVY += ay; + + // When approaching the target direction, decelerate smoothly: + // from the relation between acceleration, speed and distance, compute + // the maximum speed that can still be stopped with the configured + // acceleration, and clamp the current speed to it. + { + // Relation between acceleration, velocity and distance. + // 2 6 2 3 + // sqrt(a t + 16 a h t - 8 a h) - a t + // v = -------------------------------------- + // 2 + // 4 t - 2 + // (t=1) + float maxV = 0.5f * (MathF.Sqrt((MaxA * MaxA) + 16f * MaxA * d - 8f * MaxA * d) - MaxA); + float curV = MathF.Sqrt((_faceVX * _faceVX) + (_faceVY * _faceVY)); + + if (curV > maxV) + { + // Current speed > maximum speed: decelerate to the maximum. + _faceVX *= maxV / curV; + _faceVY *= maxV / curV; + } + } + _faceX += _faceVX; _faceY += _faceVY; } diff --git a/source_code/Cubism/Handlers/Live2DService.cs b/source_code/Cubism/Handlers/Live2DService.cs index f4dc3de..3c6d108 100644 --- a/source_code/Cubism/Handlers/Live2DService.cs +++ b/source_code/Cubism/Handlers/Live2DService.cs @@ -1,4 +1,4 @@ -using Godot; +using Godot; using Cthangover.Live2D.Cubism.Framework; namespace Cthangover.Live2D.Cubism.Services @@ -19,6 +19,13 @@ namespace Cthangover.Live2D.Cubism.Services public float AnchorX = 0.5f; public float AnchorY = 0.5f; public FitMode ScaleFit = FitMode.FitAuto; + + /// + /// Enables the automatic eye-blink effect. Disable for pixel-deterministic + /// rendering (screenshot/visual tests): the blink fires on a random timer + /// and would otherwise change pixels between captures. + /// + public bool EnableEyeBlink = true; } /// @@ -68,7 +75,8 @@ namespace Cthangover.Live2D.Cubism.Services Name = config.NodeName, AnchorX = config.AnchorX, AnchorY = config.AnchorY, - ScaleFit = fitMode + ScaleFit = fitMode, + EnableEyeBlink = config.EnableEyeBlink }; parent.AddChild(model); diff --git a/source_code/Properties/AssemblyInfo.cs b/source_code/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..efa1b26 --- /dev/null +++ b/source_code/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Runtime.CompilerServices; + +// Exposes internal pure-math/parsing helpers of the Live2D mod to the game's +// unit test project (tests/Cthangover.Tests). Internal members are the test +// seam; public runtime API stays unchanged. +[assembly: InternalsVisibleTo("Cthangover.Tests")] diff --git a/src/AddLive2DModelAction.cs b/src/AddLive2DModelAction.cs index d1e00e8..f427e8a 100644 --- a/src/AddLive2DModelAction.cs +++ b/src/AddLive2DModelAction.cs @@ -4,6 +4,7 @@ using Cthangover.Core.Utils; using Godot; using Cthangover.Live2D.Cubism.Services; +using Cthangover.Mods.Interface.View; namespace Mods.Live2D { public class AddLive2DModelAction : IScenarioAction