upd: sync local changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b8ddf1f0df
commit
a9d4417fce
27 files changed
+3446
-1148
No files matched your search
+257
-46
@@ -33,117 +33,258 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.CodeAnalysis.Common/4.11.0": {
|
"Humanizer.Core/2.2.0": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"lib/net8.0/Microsoft.CodeAnalysis.dll": {
|
"lib/netstandard1.0/Humanizer.dll": {
|
||||||
"assemblyVersion": "4.11.0.0",
|
"assemblyVersion": "2.2.0.0",
|
||||||
"fileVersion": "4.1100.24.37604"
|
"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": {
|
"resources": {
|
||||||
"lib/net8.0/cs/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "cs"
|
"locale": "cs"
|
||||||
},
|
},
|
||||||
"lib/net8.0/de/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "de"
|
"locale": "de"
|
||||||
},
|
},
|
||||||
"lib/net8.0/es/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "es"
|
"locale": "es"
|
||||||
},
|
},
|
||||||
"lib/net8.0/fr/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "fr"
|
"locale": "fr"
|
||||||
},
|
},
|
||||||
"lib/net8.0/it/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "it"
|
"locale": "it"
|
||||||
},
|
},
|
||||||
"lib/net8.0/ja/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "ja"
|
"locale": "ja"
|
||||||
},
|
},
|
||||||
"lib/net8.0/ko/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "ko"
|
"locale": "ko"
|
||||||
},
|
},
|
||||||
"lib/net8.0/pl/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "pl"
|
"locale": "pl"
|
||||||
},
|
},
|
||||||
"lib/net8.0/pt-BR/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/pt-BR/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "pt-BR"
|
"locale": "pt-BR"
|
||||||
},
|
},
|
||||||
"lib/net8.0/ru/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "ru"
|
"locale": "ru"
|
||||||
},
|
},
|
||||||
"lib/net8.0/tr/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "tr"
|
"locale": "tr"
|
||||||
},
|
},
|
||||||
"lib/net8.0/zh-Hans/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/zh-Hans/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "zh-Hans"
|
"locale": "zh-Hans"
|
||||||
},
|
},
|
||||||
"lib/net8.0/zh-Hant/Microsoft.CodeAnalysis.resources.dll": {
|
"lib/netcoreapp3.1/zh-Hant/Microsoft.CodeAnalysis.resources.dll": {
|
||||||
"locale": "zh-Hant"
|
"locale": "zh-Hant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.CodeAnalysis.CSharp/4.11.0": {
|
"Microsoft.CodeAnalysis.CSharp/3.11.0": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.CodeAnalysis.Common": "4.11.0"
|
"Microsoft.CodeAnalysis.Common": "3.11.0"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"lib/net8.0/Microsoft.CodeAnalysis.CSharp.dll": {
|
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {
|
||||||
"assemblyVersion": "4.11.0.0",
|
"assemblyVersion": "3.11.0.0",
|
||||||
"fileVersion": "4.1100.24.37604"
|
"fileVersion": "3.1100.21.40306"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"lib/net8.0/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "cs"
|
"locale": "cs"
|
||||||
},
|
},
|
||||||
"lib/net8.0/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "de"
|
"locale": "de"
|
||||||
},
|
},
|
||||||
"lib/net8.0/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "es"
|
"locale": "es"
|
||||||
},
|
},
|
||||||
"lib/net8.0/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "fr"
|
"locale": "fr"
|
||||||
},
|
},
|
||||||
"lib/net8.0/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "it"
|
"locale": "it"
|
||||||
},
|
},
|
||||||
"lib/net8.0/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "ja"
|
"locale": "ja"
|
||||||
},
|
},
|
||||||
"lib/net8.0/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "ko"
|
"locale": "ko"
|
||||||
},
|
},
|
||||||
"lib/net8.0/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "pl"
|
"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"
|
"locale": "pt-BR"
|
||||||
},
|
},
|
||||||
"lib/net8.0/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "ru"
|
"locale": "ru"
|
||||||
},
|
},
|
||||||
"lib/net8.0/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
"lib/netcoreapp3.1/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
|
||||||
"locale": "tr"
|
"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"
|
"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"
|
"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": {
|
"Cthangover/1.0.0": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"GodotSharp": "4.6.3",
|
"GodotSharp": "4.6.3",
|
||||||
"GodotSharpEditor": "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": {
|
"runtime": {
|
||||||
"Cthangover.dll": {
|
"Cthangover.dll": {
|
||||||
"assemblyVersion": "0.0.0.0",
|
"assemblyVersion": "1.0.0.0",
|
||||||
"fileVersion": "0.0.0.0"
|
"fileVersion": "1.0.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,19 +310,89 @@
|
|||||||
"path": "godotsharpeditor/4.6.3",
|
"path": "godotsharpeditor/4.6.3",
|
||||||
"hashPath": "godotsharpeditor.4.6.3.nupkg.sha512"
|
"hashPath": "godotsharpeditor.4.6.3.nupkg.sha512"
|
||||||
},
|
},
|
||||||
"Microsoft.CodeAnalysis.Common/4.11.0": {
|
"Humanizer.Core/2.2.0": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
"sha512": "sha512-djf8ujmqYImFgB04UGtcsEhHrzVqzHowS+EEl/Yunc5LdrYrZhGBWUTXoCF0NzYXJxtfuD+UVQarWpvrNc94Qg==",
|
"sha512": "sha512-rsYXB7+iUPP8AHgQ8JP2UZI2xK2KhjcdGr9E6zX3CsZaTLCaw8M35vaAJRo1rfxeaZEVMuXeaquLVCkZ7JcZ5Q==",
|
||||||
"path": "microsoft.codeanalysis.common/4.11.0",
|
"path": "humanizer.core/2.2.0",
|
||||||
"hashPath": "microsoft.codeanalysis.common.4.11.0.nupkg.sha512"
|
"hashPath": "humanizer.core.2.2.0.nupkg.sha512"
|
||||||
},
|
},
|
||||||
"Microsoft.CodeAnalysis.CSharp/4.11.0": {
|
"Microsoft.Bcl.AsyncInterfaces/5.0.0": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
"sha512": "sha512-6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==",
|
"sha512": "sha512-W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==",
|
||||||
"path": "microsoft.codeanalysis.csharp/4.11.0",
|
"path": "microsoft.bcl.asyncinterfaces/5.0.0",
|
||||||
"hashPath": "microsoft.codeanalysis.csharp.4.11.0.nupkg.sha512"
|
"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": {
|
"Cthangover/1.0.0": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
|||||||
|
mod_live2d_name = Live2D Cubism
|
||||||
|
mod_live2d_desc = Live2D Cubism C# runtime: model loading, rendering, physics, animation, effects.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
mod_live2d_name = Live2D Cubism
|
||||||
|
mod_live2d_desc = C#-рантайм Live2D Cubism: загрузка и отрисовка моделей, физика, анимации и эффекты.
|
||||||
+2
-3
@@ -5,7 +5,6 @@
|
|||||||
"author": "ct",
|
"author": "ct",
|
||||||
"description": "Live2D Cubism C# runtime: model loading, rendering, physics, animation, effects",
|
"description": "Live2D Cubism C# runtime: model loading, rendering, physics, animation, effects",
|
||||||
"category": "library, visual",
|
"category": "library, visual",
|
||||||
"depends": ["visual_novel_core>=1"],
|
"depends": ["visual_novel_core>=1", "interface>=1"],
|
||||||
"depends_native": ["native/*.dll"],
|
"depends_native": ["native/*.dll", "native/*.so"]
|
||||||
"sources": ["src/*.cs"]
|
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
Binary file not shown.
@@ -71,6 +71,19 @@ namespace Cthangover.Live2D.Cubism.Core
|
|||||||
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||||
public static extern unsafe IntPtr csmReviveMocInPlace(void* address, uint size);
|
public static extern unsafe IntPtr csmReviveMocInPlace(void* address, uint size);
|
||||||
|
|
||||||
|
/// <summary>Native log callback: receives a pointer to a NUL-terminated csmChar* message.</summary>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
public delegate void CsmLogDelegate(IntPtr messagePtr);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||||
|
public static extern void csmSetLogFunction(CsmLogDelegate logFunction);
|
||||||
|
|
||||||
/// <summary>Queries the memory size needed to initialize a model from the given moc.</summary>
|
/// <summary>Queries the memory size needed to initialize a model from the given moc.</summary>
|
||||||
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||||
public static extern unsafe uint csmGetSizeofModel(IntPtr moc);
|
public static extern unsafe uint csmGetSizeofModel(IntPtr moc);
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Cthangover.Live2D.Cubism.Framework
|
namespace Cthangover.Live2D.Cubism.Framework
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abstract base for all Live2D Cubism motions (animations and expressions).
|
/// Abstract base for all Live2D Cubism motions (animations and expressions).
|
||||||
/// Provides fade-in/fade-out timing, looping, weight blending, and event
|
/// Порт ACubismMotion из Cubism SDK for Native 5-r.5: владеет временами фейда,
|
||||||
/// callbacks shared across <see cref="CubismMotion"/> and
|
/// весом, цикличностью и колбэками; очередь (<see cref="CubismMotionQueueManager"/>)
|
||||||
/// <see cref="CubismExpressionMotion"/>.
|
/// вызывает <see cref="UpdateParameters"/> раз в кадр, тот в свою очередь —
|
||||||
///
|
/// <see cref="SetupMotionQueueEntry"/> (при старте), <see cref="UpdateFadeWeight"/>
|
||||||
/// Motions are managed by <see cref="CubismMotionQueueManager"/>, which
|
/// и виртуальный <see cref="DoUpdateParameters"/>.
|
||||||
/// calls <see cref="SetupMotionQueueEntry"/> on start, then
|
///
|
||||||
/// <see cref="UpdateFadeWeight"/> and <see cref="DoUpdateParameters"/>
|
/// Завершение выставляется по EndTime: сам моушен ставит IsFinished в
|
||||||
/// every frame.
|
/// DoUpdateParameters (незацикленный), а обёртка <see cref="UpdateParameters"/>
|
||||||
|
/// после применения параметров закрывает запись, когда текущее время прошло EndTime
|
||||||
|
/// (например, после запущенного через очередь fade-out). Колбэки при этом не
|
||||||
|
/// дублируются: OnFinishedMotion вызывается только самим моушеном.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ACubismMotion
|
public abstract class ACubismMotion
|
||||||
{
|
{
|
||||||
@@ -36,6 +40,9 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
protected bool _isLoopFadeIn = true;
|
protected bool _isLoopFadeIn = true;
|
||||||
protected bool _previousLoopState;
|
protected bool _previousLoopState;
|
||||||
|
|
||||||
|
/// <summary>Сработавшие события (UserData); наполняется в <see cref="GetFiredEvent"/>.</summary>
|
||||||
|
protected readonly List<string> _firedEventValues = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Duration of the fade-in transition in seconds.
|
/// Duration of the fade-in transition in seconds.
|
||||||
/// A negative value means the default (1 second) will be used
|
/// A negative value means the default (1 second) will be used
|
||||||
@@ -75,8 +82,8 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked when the motion finishes (non-looping only).
|
/// Invoked when the motion finishes (non-looping only).
|
||||||
/// Automatically called by <see cref="CubismMotion.DoUpdateParameters"/>
|
/// Автоматически вызывается самим моушеном в DoUpdateParameters по достижении
|
||||||
/// when timeOffset exceeds duration.
|
/// длительности; очередь этот колбэк не дублирует.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public FinishedMotionCallback OnFinishedMotion { get; set; }
|
public FinishedMotionCallback OnFinishedMotion { get; set; }
|
||||||
|
|
||||||
@@ -101,73 +108,111 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
public virtual float GetLoopDuration() => -1f;
|
public virtual float GetLoopDuration() => -1f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a queue entry with start/end times based on current
|
/// Порт ACubismMotion::UpdateParameters — кадровая обёртка, вызываемая очередью.
|
||||||
/// <see cref="FadeInSeconds"/>, <see cref="OffsetSeconds"/>,
|
/// Проверяет доступность записи, инициализирует её при старте, считает fade-вес,
|
||||||
/// and <see cref="GetDuration"/>. Fires <see cref="OnBeganMotion"/>.
|
/// применяет параметры и после этого закрывает запись, если время прошло EndTime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetupMotionQueueEntry(CubismMotionQueueEntry entry, float userTimeSeconds)
|
public void UpdateParameters(CubismNativeModel model, CubismMotionQueueEntry entry, float userTimeSeconds)
|
||||||
{
|
{
|
||||||
var fadeIn = _fadeInSeconds >= 0f ? _fadeInSeconds : 1f;
|
if (!entry.IsAvailable || entry.IsFinished)
|
||||||
entry.StartTime = userTimeSeconds - _offsetSeconds;
|
|
||||||
entry.FadeInStartTime = userTimeSeconds;
|
|
||||||
var duration = GetDuration();
|
|
||||||
UpdateEndTime(entry, duration);
|
|
||||||
OnBeganMotion?.Invoke(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Computes the final frame weight = <see cref="Weight"/> x fadeIn x fadeOut.
|
|
||||||
/// Fade curves use the sine easing function from <see cref="CubismMath.GetEasingSine"/>.
|
|
||||||
/// </summary>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the queue entry's EndTime based on loop state and duration.
|
|
||||||
/// Looping motions get EndTime = -1 (infinite).
|
|
||||||
/// </summary>
|
|
||||||
protected void UpdateEndTime(CubismMotionQueueEntry entry, float duration)
|
|
||||||
{
|
|
||||||
entry.EndTime = _isLoop ? -1f : entry.StartTime + duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Recalculates EndTime when <see cref="IsLoop"/> changes mid-playback.
|
|
||||||
/// Called by <see cref="CubismMotion.DoUpdateParameters"/> every frame
|
|
||||||
/// to detect loop state transitions.
|
|
||||||
/// </summary>
|
|
||||||
protected void AdjustEndTime(CubismMotionQueueEntry entry, float duration)
|
|
||||||
{
|
|
||||||
if (_isLoop != _previousLoopState)
|
|
||||||
{
|
{
|
||||||
_previousLoopState = _isLoop;
|
return;
|
||||||
UpdateEndTime(entry, duration);
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Порт ACubismMotion::SetupMotionQueueEntry: при первом кадре записывает
|
||||||
|
/// StartTime (с учётом OffsetSeconds), FadeInStartTime и EndTime
|
||||||
|
/// (если EndTime ещё не выставлен извне, например фейдаутом от очереди).
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Порт ACubismMotion::UpdateFadeWeight: вес = Weight x fadeIn x fadeOut
|
||||||
|
/// с синус-эйзингом. При fadeIn/fadeOut == 0 соответствующий множитель = 1.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Порт ACubismMotion::AdjustEndTime: EndTime = StartTime + GetDuration();
|
||||||
|
/// для цикличных моушенов (GetDuration() <= 0) EndTime = -1 (бесконечность).
|
||||||
|
/// </summary>
|
||||||
|
protected void AdjustEndTime(CubismMotionQueueEntry entry)
|
||||||
|
{
|
||||||
|
float duration = GetDuration();
|
||||||
|
float endTime = duration <= 0f ? -1f : entry.StartTime + duration;
|
||||||
|
entry.EndTime = endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Порт ACubismMotion::GetFiredEvent: базовая реализация не содержит событий.
|
||||||
|
/// <see cref="CubismMotion"/> переопределяет и наполняет список по UserData.
|
||||||
|
/// </summary>
|
||||||
|
public virtual List<string> GetFiredEvent(float beforeCheckTimeSeconds, float motionTimeSeconds)
|
||||||
|
{
|
||||||
|
return _firedEventValues;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies motion parameter values to the model for the current frame.
|
/// Applies motion parameter values to the model for the current frame.
|
||||||
/// Called every frame by <see cref="CubismMotionQueueManager.DoUpdateMotion"/>.
|
/// Called every frame by <see cref="ACubismMotion.UpdateParameters"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="model">The native Cubism model to modify.</param>
|
/// <param name="model">The native Cubism model to modify.</param>
|
||||||
/// <param name="userTimeSeconds">Elapsed user time in seconds.</param>
|
/// <param name="userTimeSeconds">Elapsed user time in seconds.</param>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
@@ -30,19 +29,27 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Expression motion, parsed from a Cubism .exp3.json file.
|
/// Expression motion, parsed from a Cubism .exp3.json file.
|
||||||
/// Inherits fade and weight management from <see cref="ACubismMotion"/>.
|
/// Inherits fade and weight management from <see cref="ACubismMotion"/>.
|
||||||
///
|
///
|
||||||
/// Expressions are single-frame poses that transition between values using
|
/// Формулы применения — точный порт CubismExpressionMotion::DoUpdateParameters
|
||||||
/// one of three blend modes per parameter:
|
/// через Set/Add/MultiplyParameterValue из CubismModel (SDK 5-r.5), weight — fade-вес:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><see cref="ExpressionBlendType.Additive"/> — adds <c>value * weight</c></item>
|
/// <item><see cref="ExpressionBlendType.Additive"/> — <c>value + expValue * weight</c></item>
|
||||||
/// <item><see cref="ExpressionBlendType.Multiply"/> — multiplies by <c>1 + value * weight</c></item>
|
/// <item><see cref="ExpressionBlendType.Multiply"/> — <c>value * (1 + (expValue - 1) * weight)</c></item>
|
||||||
/// <item><see cref="ExpressionBlendType.Overwrite"/> — lerps from current to target using weight</item>
|
/// <item><see cref="ExpressionBlendType.Overwrite"/> — <c>value * (1 - weight) + expValue * weight</c></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
///
|
///
|
||||||
/// Loaded and started by <see cref="CubismModelNode.StartExpression"/>.
|
/// Как и в SDK, результат зажимается в [min, max] параметра (порт
|
||||||
|
/// CubismModel::SetParameterValue), а параметры применяются в порядке их
|
||||||
|
/// перечисления в exp3.json: при нескольких записях с одним Id побеждает последняя.
|
||||||
|
/// Fade-время по умолчанию — 1 c (DefaultFadeTime), если в файле не задано.
|
||||||
|
///
|
||||||
|
/// Загружается и запускается через <see cref="CubismModelNode.StartExpression"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CubismExpressionMotion : ACubismMotion
|
public class CubismExpressionMotion : ACubismMotion
|
||||||
{
|
{
|
||||||
|
/// <summary>Порт DefaultFadeTime из CubismExpressionMotion.cpp.</summary>
|
||||||
|
private const float DefaultFadeTime = 1f;
|
||||||
|
|
||||||
private readonly List<ExpressionParameter> _parameters = new();
|
private readonly List<ExpressionParameter> _parameters = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -55,23 +62,23 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
var json = JsonDocument.Parse(jsonBytes);
|
var json = JsonDocument.Parse(jsonBytes);
|
||||||
var root = json.RootElement;
|
var root = json.RootElement;
|
||||||
|
|
||||||
if (root.TryGetProperty("FadeInTime", out var fi))
|
// Порт CubismExpressionMotion::Parse: root[FadeInTime].ToFloat(DefaultFadeTime).
|
||||||
_fadeInSeconds = fi.GetSingle();
|
_fadeInSeconds = root.TryGetProperty("FadeInTime", out var fi) && fi.ValueKind == JsonValueKind.Number ? fi.GetSingle() : DefaultFadeTime;
|
||||||
if (root.TryGetProperty("FadeOutTime", out var fo))
|
_fadeOutSeconds = root.TryGetProperty("FadeOutTime", out var fo) && fo.ValueKind == JsonValueKind.Number ? fo.GetSingle() : DefaultFadeTime;
|
||||||
_fadeOutSeconds = fo.GetSingle();
|
|
||||||
|
|
||||||
if (root.TryGetProperty("Parameters", out var paramsArr))
|
if (root.TryGetProperty("Parameters", out var paramsArr))
|
||||||
{
|
{
|
||||||
foreach (var p in paramsArr.EnumerateArray())
|
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())
|
? ParseBlendType(b.GetString())
|
||||||
: ExpressionBlendType.Additive;
|
: ExpressionBlendType.Additive;
|
||||||
|
|
||||||
_parameters.Add(new ExpressionParameter
|
_parameters.Add(new ExpressionParameter
|
||||||
{
|
{
|
||||||
ParameterId = p.GetProperty("Id").GetString(),
|
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
|
BlendType = blend
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -96,30 +103,41 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
var values = model.GetParameterValues();
|
var values = model.GetParameterValues();
|
||||||
var minValues = model.GetParameterMinimumValues();
|
var minValues = model.GetParameterMinimumValues();
|
||||||
var maxValues = model.GetParameterMaximumValues();
|
var maxValues = model.GetParameterMaximumValues();
|
||||||
var defaultValues = model.GetParameterDefaultValues();
|
|
||||||
|
|
||||||
for (int pi = 0; pi < model.ParameterCount; pi++)
|
var indexById = new Dictionary<string, int>(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);
|
if (!indexById.TryGetValue(ep.ParameterId, out int pi)) continue;
|
||||||
foreach (var ep in _parameters)
|
|
||||||
{
|
|
||||||
if (ep.ParameterId != pid) continue;
|
|
||||||
|
|
||||||
switch (ep.BlendType)
|
switch (ep.BlendType)
|
||||||
{
|
{
|
||||||
case ExpressionBlendType.Additive:
|
case ExpressionBlendType.Additive:
|
||||||
values[pi] = values[pi] + ep.Value * weight;
|
// AddParameterValue: value + (expValue * weight)
|
||||||
break;
|
values[pi] = ClampParameterValue(values[pi] + ep.Value * weight, minValues[pi], maxValues[pi]);
|
||||||
case ExpressionBlendType.Multiply:
|
break;
|
||||||
values[pi] = values[pi] * (1f + ep.Value * weight);
|
case ExpressionBlendType.Multiply:
|
||||||
break;
|
// MultiplyParameterValue: value * (1 + (expValue - 1) * weight)
|
||||||
case ExpressionBlendType.Overwrite:
|
values[pi] = ClampParameterValue(values[pi] * (1f + (ep.Value - 1f) * weight), minValues[pi], maxValues[pi]);
|
||||||
values[pi] = values[pi] * (1f - weight) + ep.Value * weight;
|
break;
|
||||||
break;
|
case ExpressionBlendType.Overwrite:
|
||||||
}
|
// SetParameterValue(id, value, weight): expValue зажимается, затем лерп
|
||||||
break;
|
values[pi] = values[pi] * (1f - weight)
|
||||||
|
+ ClampParameterValue(ep.Value, minValues[pi], maxValues[pi]) * weight;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Порт GetParameterClampValue — зажим значения параметра в [min, max].</summary>
|
||||||
|
private static float ClampParameterValue(float value, float min, float max)
|
||||||
|
{
|
||||||
|
if (value < min) return min;
|
||||||
|
if (value > max) return max;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,20 +5,29 @@ using Godot;
|
|||||||
namespace Cthangover.Live2D.Cubism.Framework
|
namespace Cthangover.Live2D.Cubism.Framework
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages the 2D transformation (scale, translation, alignment) for a
|
/// Port of CubismModelMatrix.cpp (Cubism SDK for Native 5-r.5).
|
||||||
/// Cubism model rendered inside a Godot <see cref="Node2D"/>.
|
/// Manages the 2D transformation (scale, translation, alignment) of a Cubism
|
||||||
///
|
/// model inside its logical coordinate space.
|
||||||
/// Works with two coordinate systems:
|
///
|
||||||
|
/// SDK semantics to be aware of:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item>Cubism canvas coordinates (the model's native coordinate space)</item>
|
/// <item><see cref="SetWidth"/> and <see cref="SetHeight"/> MULTIPLY the
|
||||||
/// <item>Godot node local coordinates (where the model is placed)</item>
|
/// current scale by target / canvas-size (they do not assign it),
|
||||||
|
/// exactly like the C++ original.</item>
|
||||||
|
/// <item>The (width, height) constructor performs
|
||||||
|
/// <c>SetHeight(2.0f)</c> like the SDK, i.e. the model is initially
|
||||||
|
/// scaled to occupy 2 logical units of height.</item>
|
||||||
|
/// <item>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).</item>
|
||||||
|
/// <item>CenterX/CenterY/Bottom/Right compute their offset from the full
|
||||||
|
/// scaled size (canvas size × current scale).</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
///
|
///
|
||||||
/// The scale is computed relative to the canvas dimensions stored at construction.
|
/// <see cref="SetupFromLayout"/> reads positioning keys from a Cubism
|
||||||
/// <see cref="SetWidth"/> and <see cref="SetHeight"/> set a uniform scale based
|
/// .model3.json "Layout" section; <see cref="ApplyToNode"/> writes the final
|
||||||
/// on the ratio of target size to canvas size. <see cref="SetupFromLayout"/> reads
|
/// transform to a Godot <see cref="Node2D"/>.
|
||||||
/// positioning keys from a Cubism .model3.json "Layout" section.
|
|
||||||
/// <see cref="ApplyToNode"/> writes the final transform to a Godot Node2D.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CubismModelMatrix
|
public class CubismModelMatrix
|
||||||
{
|
{
|
||||||
@@ -33,68 +42,80 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
public CubismModelMatrix() { }
|
public CubismModelMatrix() { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a matrix referenced to the given canvas pixel dimensions.
|
/// Creates a matrix referenced to the given canvas dimensions.
|
||||||
/// Subsequent calls to <see cref="SetWidth"/> and <see cref="SetHeight"/>
|
/// Like the SDK constructor, this calls <see cref="SetHeight"/>(2.0f),
|
||||||
/// will compute scale as target / canvas.
|
/// so the model initially occupies 2 logical units of height.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CubismModelMatrix(float width, float height)
|
public CubismModelMatrix(float width, float height)
|
||||||
{
|
{
|
||||||
_width = width;
|
_width = width;
|
||||||
_height = height;
|
_height = height;
|
||||||
|
|
||||||
|
SetHeight(2.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Current uniform scale on the X axis.</summary>
|
/// <summary>Current scale on the X axis.</summary>
|
||||||
public float ScaleX => _scaleX;
|
public float ScaleX => _scaleX;
|
||||||
|
|
||||||
/// <summary>Current uniform scale on the Y axis.</summary>
|
/// <summary>Current scale on the Y axis.</summary>
|
||||||
public float ScaleY => _scaleY;
|
public float ScaleY => _scaleY;
|
||||||
|
|
||||||
/// <summary>Current X translation offset in Godot local coordinates.</summary>
|
/// <summary>Current X translation offset.</summary>
|
||||||
public float TranslateX => _translateX;
|
public float TranslateX => _translateX;
|
||||||
|
|
||||||
/// <summary>Current Y translation offset in Godot local coordinates.</summary>
|
/// <summary>Current Y translation offset.</summary>
|
||||||
public float TranslateY => _translateY;
|
public float TranslateY => _translateY;
|
||||||
|
|
||||||
|
/// <summary>SDK CubismModelMatrix::Scale — multiplies the current scale.</summary>
|
||||||
|
public void Scale(float scaleX, float scaleY)
|
||||||
|
{
|
||||||
|
_scaleX *= scaleX;
|
||||||
|
_scaleY *= scaleY;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets uniform scale to match the given width. Scale = targetWidth / canvasWidth.
|
/// SDK SetWidth: multiplies the current uniform scale by w / canvasWidth.
|
||||||
/// Does nothing if canvas width is zero or unknown.
|
/// Ignored when the canvas width is zero or unknown (the SDK would divide by zero).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetWidth(float w)
|
public void SetWidth(float w)
|
||||||
{
|
{
|
||||||
if (_width > 0f)
|
if (_width <= 0f) return;
|
||||||
{
|
|
||||||
var scale = w / _width;
|
var scaleX = w / _width;
|
||||||
_scaleX = scale;
|
var scaleY = scaleX;
|
||||||
_scaleY = scale;
|
Scale(scaleX, scaleY);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets uniform scale to match the given height. Scale = targetHeight / canvasHeight.
|
/// SDK SetHeight: multiplies the current uniform scale by h / canvasHeight.
|
||||||
/// Does nothing if canvas height is zero or unknown.
|
/// Ignored when the canvas height is zero or unknown (the SDK would divide by zero).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetHeight(float h)
|
public void SetHeight(float h)
|
||||||
{
|
{
|
||||||
if (_height > 0f)
|
if (_height <= 0f) return;
|
||||||
{
|
|
||||||
var scale = h / _height;
|
var scaleX = h / _height;
|
||||||
_scaleX = scale;
|
var scaleY = scaleX;
|
||||||
_scaleY = scale;
|
Scale(scaleX, scaleY);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Sets absolute translation position.</summary>
|
/// <summary>SDK Translate — adds x and y to the current translation
|
||||||
public void SetPosition(float x, float y)
|
/// (the SDK's TranslateX/TranslateY helpers are folded in here; the
|
||||||
|
/// TranslateX/TranslateY properties above are the read accessors).</summary>
|
||||||
|
public void Translate(float x, float y)
|
||||||
{
|
{
|
||||||
_translateX = x;
|
_translateX += x;
|
||||||
_translateY = y;
|
_translateY += y;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Sets absolute X translation.</summary>
|
/// <summary>SDK SetPosition — incremental (adds to the current translation).</summary>
|
||||||
public void SetX(float x) => _translateX = x;
|
public void SetPosition(float x, float y) => Translate(x, y);
|
||||||
|
|
||||||
/// <summary>Sets absolute Y translation.</summary>
|
/// <summary>SDK SetX — incremental (adds to the current X translation).</summary>
|
||||||
public void SetY(float y) => _translateY = y;
|
public void SetX(float x) => _translateX += x;
|
||||||
|
|
||||||
|
/// <summary>SDK SetY — incremental (adds to the current Y translation).</summary>
|
||||||
|
public void SetY(float y) => _translateY += y;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Centers the model horizontally and vertically at the given point.
|
/// Centers the model horizontally and vertically at the given point.
|
||||||
@@ -106,34 +127,45 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
CenterY(y);
|
CenterY(y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>SDK CenterX — adds x - (width × scaleX) / 2 to the X translation.</summary>
|
||||||
/// Centers horizontally: translateX = x - (width * scaleX) / 2.
|
public void CenterX(float x)
|
||||||
/// </summary>
|
{
|
||||||
public void CenterX(float x) => _translateX = x - (_width * _scaleX) / 2f;
|
var w = _width * _scaleX;
|
||||||
|
_translateX += x - (w / 2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>SDK CenterY — adds y - (height × scaleY) / 2 to the Y translation.</summary>
|
||||||
|
public void CenterY(float y)
|
||||||
|
{
|
||||||
|
var h = _height * _scaleY;
|
||||||
|
_translateY += y - (h / 2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>SDK Top — adds y to the Y translation (via SetY).</summary>
|
||||||
|
public void Top(float y) => SetY(y);
|
||||||
|
|
||||||
|
/// <summary>SDK Bottom — adds y - (height × scaleY) to the Y translation.</summary>
|
||||||
|
public void Bottom(float y)
|
||||||
|
{
|
||||||
|
var h = _height * _scaleY;
|
||||||
|
_translateY += y - h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>SDK Left — adds x to the X translation (via SetX).</summary>
|
||||||
|
public void Left(float x) => SetX(x);
|
||||||
|
|
||||||
|
/// <summary>SDK Right — adds x - (width × scaleX) to the X translation.</summary>
|
||||||
|
public void Right(float x)
|
||||||
|
{
|
||||||
|
var w = _width * _scaleX;
|
||||||
|
_translateX += x - w;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Centers vertically: translateY = y - (height * scaleY) / 2.
|
/// SDK SetupFromLayout — configures scale and position from a layout
|
||||||
/// </summary>
|
/// dictionary (lowercased keys). Width/height are applied first (they
|
||||||
public void CenterY(float y) => _translateY = y - (_height * _scaleY) / 2f;
|
/// affect scale), then the position keys: "x", "y", "center_x",
|
||||||
|
/// "center_y", "top", "bottom", "left", "right".
|
||||||
/// <summary>Aligns the top edge of the model to y.</summary>
|
|
||||||
public void Top(float y) => _translateY = y;
|
|
||||||
|
|
||||||
/// <summary>Aligns the bottom edge of the model to y.</summary>
|
|
||||||
public void Bottom(float y) => _translateY = y - _height * _scaleY;
|
|
||||||
|
|
||||||
/// <summary>Aligns the left edge of the model to x.</summary>
|
|
||||||
public void Left(float x) => _translateX = x;
|
|
||||||
|
|
||||||
/// <summary>Aligns the right edge of the model to x.</summary>
|
|
||||||
public void Right(float x) => _translateX = x - _width * _scaleX;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetupFromLayout(Dictionary<string, float> layout)
|
public void SetupFromLayout(Dictionary<string, float> layout)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Cthangover.Core.Mods;
|
|||||||
using Cthangover.Core.UI.Event;
|
using Cthangover.Core.UI.Event;
|
||||||
using Cthangover.Core.Utils;
|
using Cthangover.Core.Utils;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Cthangover.Live2D.Cubism.Framework
|
namespace Cthangover.Live2D.Cubism.Framework
|
||||||
{
|
{
|
||||||
@@ -36,6 +37,11 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
private CubismRenderer2D _renderer;
|
private CubismRenderer2D _renderer;
|
||||||
private CubismModelSettingJson _modelSetting;
|
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 CubismEyeBlink _eyeBlink;
|
||||||
private CubismBreath _breath;
|
private CubismBreath _breath;
|
||||||
private CubismPose _pose;
|
private CubismPose _pose;
|
||||||
@@ -53,7 +59,29 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
private string _modelDir;
|
private string _modelDir;
|
||||||
private IModFileProvider _fileProvider;
|
private IModFileProvider _fileProvider;
|
||||||
private bool _layoutLogged;
|
private bool _layoutLogged;
|
||||||
private readonly System.Collections.Generic.HashSet<string> _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<string, float> _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<string, int> _parameterIndexById = new();
|
||||||
|
|
||||||
|
private FaceTrackEntry[] _faceTrackCache = System.Array.Empty<FaceTrackEntry>();
|
||||||
|
|
||||||
|
private readonly List<string> _eyeBlinkIds = new();
|
||||||
|
private readonly List<string> _lipSyncIds = new();
|
||||||
|
|
||||||
|
/// <summary>One precomputed face-track injection: values[index] = targetAxis * scale.</summary>
|
||||||
|
private struct FaceTrackEntry
|
||||||
|
{
|
||||||
|
public int Index;
|
||||||
|
public string Id;
|
||||||
|
public bool FromTargetX;
|
||||||
|
public float Scale;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All model parameters with their IDs, bounds, and current values.
|
/// All model parameters with their IDs, bounds, and current values.
|
||||||
@@ -178,6 +206,13 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
|
|
||||||
_nativeModel = new CubismNativeModel();
|
_nativeModel = new CubismNativeModel();
|
||||||
_nativeModel.LoadFromProvider(jsonBytes, _modelDir, _fileProvider);
|
_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);
|
FinishSetup(jsonBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +237,10 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
|
|
||||||
var ec = GetNodeOrNull<SceneEventController>("/root/EventController");
|
var ec = GetNodeOrNull<SceneEventController>("/root/EventController");
|
||||||
if (ec != null)
|
if (ec != null)
|
||||||
|
{
|
||||||
|
_eventController = ec;
|
||||||
ec.AddUpdateEventListener(this);
|
ec.AddUpdateEventListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
GameLogger.Log("LIVE2D", $"Model loaded: {_nativeModel.ParameterCount} params, {_nativeModel.DrawableCount} drawables, canvas={_nativeModel.CanvasWidth}x{_nativeModel.CanvasHeight}");
|
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
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-frame update called by the SceneEventController.
|
/// Per-frame update called by the SceneEventController.
|
||||||
/// Execution order:
|
/// Execution order (SDK LAppModel::Update):
|
||||||
/// <list type="number">
|
/// <list type="number">
|
||||||
/// <item>Apply deferred layout (first valid frame only)</item>
|
/// <item>Apply deferred layout (first valid frame only)</item>
|
||||||
/// <item>Update <see cref="_targetPoint"/> for face tracking</item>
|
/// <item>Update <see cref="_targetPoint"/> for face tracking</item>
|
||||||
/// <item>Inject face-tracking parameters (ParamAngleX/Y, ParamEyeBallX/Y)
|
/// <item>Restore the parameter snapshot (<c>LoadParameters</c>)</item>
|
||||||
/// into the native model</item>
|
|
||||||
/// <item>Evaluate motion queue (character animations)</item>
|
/// <item>Evaluate motion queue (character animations)</item>
|
||||||
/// <item>Evaluate expression queue (facial expressions)</item>
|
/// <item>Save the parameter snapshot (<c>SaveParameters</c>) — additive layers
|
||||||
/// <item>Run effect layers in order: breath, eye blink, pose, physics</item>
|
/// (expressions, breath, face-track) start every frame from the same
|
||||||
|
/// baseline and never compound frame-over-frame</item>
|
||||||
|
/// <item>Run effect layers in SDK 5 scheduler order (ICubismUpdater.hpp):
|
||||||
|
/// eye blink → expression → look/face-track → breath → physics → pose</item>
|
||||||
/// <item>Call <c>csmUpdateModel</c> on the native model</item>
|
/// <item>Call <c>csmUpdateModel</c> on the native model</item>
|
||||||
/// <item>Update the renderer (visibility, opacity, vertex positions)</item>
|
/// <item>Update the renderer (visibility, opacity, vertex positions)</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
@@ -227,6 +267,10 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
public void OnUpdate()
|
public void OnUpdate()
|
||||||
{
|
{
|
||||||
if (!_loaded) return;
|
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 now = Time.GetTicksMsec();
|
||||||
var dt = _lastTickMsec == 0 ? 0.016f : (now - _lastTickMsec) / 1000f;
|
var dt = _lastTickMsec == 0 ? 0.016f : (now - _lastTickMsec) / 1000f;
|
||||||
@@ -237,45 +281,108 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
_userTimeSeconds += dt;
|
_userTimeSeconds += dt;
|
||||||
|
|
||||||
_targetPoint.Update(dt);
|
_targetPoint.Update(dt);
|
||||||
var tx = _targetPoint.X;
|
|
||||||
var ty = _targetPoint.Y;
|
|
||||||
|
|
||||||
unsafe
|
// LAppModel.cpp:388-398 — Load/SaveParameters bracket around the motion
|
||||||
{
|
// phase. Without it, Additive/Multiply expressions and breath deltas
|
||||||
var values = _nativeModel.GetParameterValues();
|
// accumulate frame-over-frame and drive parameters into their clamps.
|
||||||
var idsPtr = _nativeModel.GetParameterIds();
|
_nativeModel.LoadParameters();
|
||||||
|
|
||||||
for (int i = 0; i < _nativeModel.ParameterCount; i++)
|
|
||||||
{
|
|
||||||
var id = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i);
|
|
||||||
if (_manualParameters.Contains(id)) continue;
|
|
||||||
switch (id)
|
|
||||||
{
|
|
||||||
case "ParamAngleX": values[i] = tx * 30f; break;
|
|
||||||
case "ParamAngleY": values[i] = ty * 30f; break;
|
|
||||||
case "ParamEyeBallX": values[i] = tx; break;
|
|
||||||
case "ParamEyeBallY": values[i] = ty; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_motionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds);
|
_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);
|
_eyeBlink?.UpdateParameters(_nativeModel, dt);
|
||||||
_pose?.UpdateParameters(_nativeModel, dt);
|
_expressionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds);
|
||||||
|
InjectFaceTrackParameters();
|
||||||
|
_breath?.UpdateParameters(_nativeModel, dt);
|
||||||
_physics?.Evaluate(_nativeModel, dt);
|
_physics?.Evaluate(_nativeModel, dt);
|
||||||
|
_pose?.UpdateParameters(_nativeModel, dt);
|
||||||
|
|
||||||
_nativeModel.UpdateModel();
|
_nativeModel.UpdateModel();
|
||||||
_renderer.UpdateModel();
|
_renderer.UpdateModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 — <see cref="CubismNativeModel.Dispose"/> is unreachable otherwise.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="SetLiveParameter"/> 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 <see cref="OnUpdate"/>
|
||||||
|
/// keeps the addition from accumulating across frames.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores values set through <see cref="SetLiveParameter"/> after
|
||||||
|
/// <c>LoadParameters</c> resets the native parameter arrays to the snapshot.
|
||||||
|
/// </summary>
|
||||||
|
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()
|
private void DeferredApplyLayout()
|
||||||
{
|
{
|
||||||
if (_layoutLogged) return;
|
if (_layoutLogged) return;
|
||||||
if (!_contentBoundsReady) 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();
|
var parentSize = GetParentSize();
|
||||||
if (parentSize.X <= 0f || parentSize.Y <= 0f) return;
|
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}");
|
GameLogger.Log("LIVE2D", $"ApplyLayout: parentSize={parentSize} contentSize={cw}x{ch} contentCenter={_contentCenter} scale={scale} pos={Position}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the "Layout" section of model3.json through
|
||||||
|
/// <see cref="CubismModelMatrix.SetupFromLayout"/> (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.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyLayoutFromModelSetting(Dictionary<string, float> 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()
|
private Vector2 GetParentSize()
|
||||||
{
|
{
|
||||||
var parent = GetParent();
|
var parent = GetParent();
|
||||||
@@ -329,6 +466,44 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
Value = defaults[i]
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<FaceTrackEntry>(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()
|
private unsafe void SetupParts()
|
||||||
@@ -351,13 +526,20 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
|
|
||||||
private void SetupEffects()
|
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();
|
_eyeBlink = new CubismEyeBlink();
|
||||||
var ids = new System.Collections.Generic.List<string>();
|
_eyeBlink.SetParameterIds(_eyeBlinkIds);
|
||||||
for (int i = 0; i < _modelSetting.GetEyeBlinkParameterCount(); i++)
|
|
||||||
ids.Add(_modelSetting.GetEyeBlinkParameterId(i));
|
|
||||||
_eyeBlink.SetParameterIds(ids);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var physicsPath = _modelSetting.GetPhysicsFileName();
|
var physicsPath = _modelSetting.GetPhysicsFileName();
|
||||||
@@ -439,25 +621,18 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
|
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
var values = _nativeModel.GetParameterValues();
|
if (!_parameterIndexById.TryGetValue(id, out var index))
|
||||||
var idsPtr = _nativeModel.GetParameterIds();
|
|
||||||
|
|
||||||
for (int i = 0; i < _nativeModel.ParameterCount; i++)
|
|
||||||
{
|
{
|
||||||
var paramId = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i);
|
GameLogger.Log("LIVE2D", $"SetLiveParameter: parameter '{id}' not found", LogLevel.Warning);
|
||||||
if (paramId != id) continue;
|
|
||||||
|
|
||||||
values[i] = value;
|
|
||||||
_manualParameters.Add(id);
|
|
||||||
|
|
||||||
if (i < Parameters.Length)
|
|
||||||
Parameters[i].Value = value;
|
|
||||||
|
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -484,6 +659,10 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
if (fadeIn >= 0f) motion.FadeInSeconds = fadeIn;
|
if (fadeIn >= 0f) motion.FadeInSeconds = fadeIn;
|
||||||
if (fadeOut >= 0f) motion.FadeOutSeconds = fadeOut;
|
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);
|
_motionManager.StartMotion(motion, true, _userTimeSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -18,6 +18,22 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public enum CubismMotionSegmentType { Linear = 0, Bezier = 1, Stepped = 2, InverseStepped = 3 }
|
public enum CubismMotionSegmentType { Linear = 0, Bezier = 1, Stepped = 2, InverseStepped = 3 }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Данные одного сегмента кривой — порт CubismMotionSegment из CubismMotionInternal.hpp:
|
||||||
|
/// индекс первой контрольной точки в общем плоском массиве точек и тип интерполяции.
|
||||||
|
/// Соседние сегменты кривой разделяют общую точку: BasePointIndex следующего сегмента
|
||||||
|
/// равен индексу последней точки предыдущего, а оценка кривой ищет сегмент по времени
|
||||||
|
/// первой точки СЛЕДУЮЩЕГО сегмента.
|
||||||
|
/// </summary>
|
||||||
|
internal struct MotionSegmentData
|
||||||
|
{
|
||||||
|
/// <summary>Индекс первой контрольной точки сегмента в CubismMotion._points.</summary>
|
||||||
|
public int BasePointIndex;
|
||||||
|
|
||||||
|
/// <summary>Тип сегмента (Linear/Bezier/Stepped/InverseStepped).</summary>
|
||||||
|
public CubismMotionSegmentType SegmentType;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A time-value pair representing a keyframe or control point on a motion curve.
|
/// A time-value pair representing a keyframe or control point on a motion curve.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
/// curve statistics, beziers-restricted flag) and the Curves/UserData sections.
|
/// curve statistics, beziers-restricted flag) and the Curves/UserData sections.
|
||||||
///
|
///
|
||||||
/// Parsed curve segments are returned as flat float arrays (interleaved
|
/// Parsed curve segments are returned as flat float arrays (interleaved
|
||||||
/// segment-type markers and point data), decoded by <see cref="CubismMotion.ParseSegments"/>.
|
/// segment-type markers and point data), decoded by CubismMotion.ParseSegmentsInternal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CubismMotionJson
|
public class CubismMotionJson
|
||||||
{
|
{
|
||||||
@@ -40,7 +40,7 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
|
|
||||||
_duration = _meta.GetProperty("Duration").GetSingle();
|
_duration = _meta.GetProperty("Duration").GetSingle();
|
||||||
_isLoop = _meta.TryGetProperty("Loop", out var loop) && loop.GetBoolean();
|
_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;
|
_curveCount = _meta.TryGetProperty("CurveCount", out var ccEl) ? ccEl.GetInt32() : 0;
|
||||||
_totalSegmentCount = _meta.TryGetProperty("TotalSegmentCount", out var tscEl) ? tscEl.GetInt32() : 0;
|
_totalSegmentCount = _meta.TryGetProperty("TotalSegmentCount", out var tscEl) ? tscEl.GetInt32() : 0;
|
||||||
_totalPointCount = _meta.TryGetProperty("TotalPointCount", out var tpcEl) ? tpcEl.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.
|
/// Motion-level fade-in time in seconds, or -1 if not specified.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float GetFadeInTime() =>
|
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;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Motion-level fade-out time in seconds, or -1 if not specified.
|
/// Motion-level fade-out time in seconds, or -1 if not specified.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float GetFadeOutTime() =>
|
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;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the target string for the curve at the given index
|
/// 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.
|
/// Per-curve fade-in time in seconds, or -1 if not specified.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float GetCurveFadeInTime(int index) =>
|
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;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-curve fade-out time in seconds, or -1 if not specified.
|
/// Per-curve fade-out time in seconds, or -1 if not specified.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float GetCurveFadeOutTime(int index) =>
|
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;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads the raw float array of segment data for a curve.
|
/// Reads the raw float array of segment data for a curve.
|
||||||
/// The first element is the segment type marker (cast to int),
|
/// Формат Segments из motion3.json: [t0, v0, type, t, v, ...] — первые ДВА
|
||||||
/// followed by point data depending on the segment type.
|
/// элемента это стартовая точка кривой (время, значение), далее для каждого
|
||||||
|
/// сегмента маркер типа и его точки (Linear/Stepped/InverseStepped — 1 точка,
|
||||||
|
/// Bezier — 3). Разбор: <see cref="CubismMotion.ParseSegmentsInternal"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void GetCurveSegments(int index, out float[] segments)
|
public void GetCurveSegments(int index, out float[] segments)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,33 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Cthangover.Live2D.Cubism.Framework
|
namespace Cthangover.Live2D.Cubism.Framework
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents one enqueued motion in the playback queue.
|
/// Represents one enqueued motion in the playback queue.
|
||||||
/// Holds timing data (start time, end time, fade-in start time),
|
/// 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.
|
/// reference to the motion itself.
|
||||||
///
|
///
|
||||||
/// Created by <see cref="CubismMotionQueueManager.StartMotion"/>
|
/// Created by <see cref="CubismMotionQueueManager.StartMotion"/>
|
||||||
/// and consumed frame-by-frame in <see cref="CubismMotionQueueManager.DoUpdateMotion"/>.
|
/// and consumed frame-by-frame in <see cref="CubismMotionQueueManager.DoUpdateMotion"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CubismMotionQueueEntry
|
public class CubismMotionQueueEntry
|
||||||
{
|
{
|
||||||
/// <summary>Absolute user time when motion playback begins.</summary>
|
/// <summary>Absolute user time when motion playback begins. -1 until the first update.</summary>
|
||||||
public float StartTime;
|
public float StartTime = -1f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Absolute user time when playback ends. -1 for infinite (looping) motions.
|
/// Absolute user time when playback ends. -1 for infinite (looping) motions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float EndTime;
|
public float EndTime = -1f;
|
||||||
|
|
||||||
/// <summary>Absolute user time when fade-in begins (may differ from StartTime for offset motions).</summary>
|
/// <summary>Absolute user time when fade-in begins (may differ from StartTime for offset motions).</summary>
|
||||||
public float FadeInStartTime;
|
public float FadeInStartTime;
|
||||||
|
|
||||||
/// <summary>Whether this entry should be processed during updates.</summary>
|
/// <summary>Whether this entry should be processed during updates.</summary>
|
||||||
public bool IsAvailable;
|
public bool IsAvailable = true;
|
||||||
|
|
||||||
/// <summary>Flag set when playback completes. Checked by the queue manager during cleanup.</summary>
|
/// <summary>Flag set when playback completes. Checked by the queue manager during cleanup.</summary>
|
||||||
public bool IsFinished;
|
public bool IsFinished;
|
||||||
|
|
||||||
|
/// <summary>Flag set on the first update — порт CubismMotionQueueEntry::_started.</summary>
|
||||||
|
public bool IsStarted;
|
||||||
|
|
||||||
|
/// <summary>Время последнего кадра (состояние), порт CubismMotionQueueEntry::SetState.</summary>
|
||||||
|
public float StateTime;
|
||||||
|
|
||||||
|
/// <summary>Вес последнего кадра (состояние), порт CubismMotionQueueEntry::SetState.</summary>
|
||||||
|
public float StateWeight;
|
||||||
|
|
||||||
|
/// <summary>Абсолютное время последней проверки событий UserData (сдвигается каждый кадр).</summary>
|
||||||
|
public float LastCheckEventTime;
|
||||||
|
|
||||||
|
/// <summary>Длительность фейда-аута, назначенного при вытеснении новым моушеном.</summary>
|
||||||
|
public float FadeOutSeconds;
|
||||||
|
|
||||||
|
/// <summary>Флаг вытеснения: фейд-аут начат, EndTime будет назначен при следующем обновлении.</summary>
|
||||||
|
public bool IsTriggeredFadeOut;
|
||||||
|
|
||||||
/// <summary>The motion instance driving parameter updates.</summary>
|
/// <summary>The motion instance driving parameter updates.</summary>
|
||||||
public ACubismMotion Motion;
|
public ACubismMotion Motion;
|
||||||
|
|
||||||
@@ -45,29 +66,70 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
{
|
{
|
||||||
Motion = motion;
|
Motion = motion;
|
||||||
AutoDelete = autoDelete;
|
AutoDelete = autoDelete;
|
||||||
IsAvailable = true;
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Порт CubismMotionQueueEntry::SetFadeout: помечает запись для фейда-аута
|
||||||
|
/// (вызывается очередью, когда стартует новый моушен).
|
||||||
|
/// </summary>
|
||||||
|
public void SetFadeout(float fadeOutSeconds)
|
||||||
|
{
|
||||||
|
FadeOutSeconds = fadeOutSeconds;
|
||||||
|
IsTriggeredFadeOut = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Порт CubismMotionQueueEntry::StartFadeout: назначает EndTime, сокращая его,
|
||||||
|
/// если фейд-аут уже был запущен раньше и раньше должен закончиться.
|
||||||
|
/// </summary>
|
||||||
|
public void StartFadeout(float fadeOutSeconds, float userTimeSeconds)
|
||||||
|
{
|
||||||
|
float newEndTimeSeconds = userTimeSeconds + fadeOutSeconds;
|
||||||
|
IsTriggeredFadeOut = true;
|
||||||
|
|
||||||
|
if (EndTime < 0f || newEndTimeSeconds < EndTime)
|
||||||
|
{
|
||||||
|
EndTime = newEndTimeSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Порт CubismMotionQueueEntry::SetState — сохраняет время и вес последнего кадра.</summary>
|
||||||
|
public void SetState(float timeSeconds, float weight)
|
||||||
|
{
|
||||||
|
StateTime = timeSeconds;
|
||||||
|
StateWeight = weight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages a queue of <see cref="ACubismMotion"/> instances for a single model.
|
/// Manages a queue of <see cref="ACubismMotion"/> instances for a single model.
|
||||||
/// Supports priority reservation (higher-priority motions preempt lower ones),
|
/// Поддержка вытеснения по приоритету (ReserveMotion), параллельного проигрывания
|
||||||
/// concurrent playback of multiple motions, and automatic cleanup of finished entries.
|
/// и автосборки завершённых записей. Порт CubismMotionQueueManager из SDK 5-r.5:
|
||||||
///
|
///
|
||||||
/// Two instances are used by <see cref="CubismModelNode"/>:
|
/// - StartMotion назначает всем прежним записям фейд-аут;
|
||||||
/// one for character motions (animations) and one for expressions.
|
/// - каждый кадр DoUpdateMotion вызывает <see cref="ACubismMotion.UpdateParameters"/>,
|
||||||
/// Each is updated independently in the per-frame loop, allowing motions
|
/// затем проверяет события UserData (<see cref="ACubismMotion.GetFiredEvent"/>) и
|
||||||
/// and expressions to blend independently with their own fade weights.
|
/// удаляет finished-записи;
|
||||||
///
|
/// - колбэком завершения владеет сам моушен: очередь его НЕ дублирует.
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CubismMotionQueueManager
|
public class CubismMotionQueueManager
|
||||||
{
|
{
|
||||||
private readonly System.Collections.Generic.List<CubismMotionQueueEntry> _entries = new();
|
private readonly List<CubismMotionQueueEntry> _entries = new();
|
||||||
|
private readonly List<string> _firedEventValues = new();
|
||||||
private int _reservedPriority = -1;
|
private int _reservedPriority = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Событие, вызываемое для каждого сработавшего UserData-события за кадр
|
||||||
|
/// (порт _eventCallback из CubismMotionQueueManager).
|
||||||
|
/// </summary>
|
||||||
|
public event Action<ACubismMotion, string> EventFired;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// События, собранные за последний кадр DoUpdateMotion по всем записям очереди.
|
||||||
|
/// Список очищается в начале каждого обновления — читайте его сразу после вызова.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> FiredEventValues => _firedEventValues;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the reserved priority threshold.
|
/// Sets the reserved priority threshold.
|
||||||
/// Motions with priority below this are rejected by <see cref="ReserveMotion"/>.
|
/// Motions with priority below this are rejected by <see cref="ReserveMotion"/>.
|
||||||
@@ -89,21 +151,27 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates and enqueues a motion for playback.
|
/// Enqueues a motion for playback. Прежним записям назначается фейд-аут
|
||||||
/// Instantiates a <see cref="CubismMotionQueueEntry"/>, calls
|
/// (SetFadeout), после чего они плавно завершаются; сама запись инициализируется
|
||||||
/// <see cref="ACubismMotion.SetupMotionQueueEntry"/> on it, and
|
/// при первом кадре <see cref="ACubismMotion.UpdateParameters"/>.
|
||||||
/// adds it to the queue. Returns the entry for optional tracking.
|
/// Параметр userTimeSeconds оставлен для совместимости вызовов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="motion">The motion to play.</param>
|
/// <param name="motion">The motion to play.</param>
|
||||||
/// <param name="autoDelete">Whether to auto-remove on finish.</param>
|
/// <param name="autoDelete">Whether to auto-remove on finish.</param>
|
||||||
/// <param name="userTimeSeconds">Current user time for start time calculation.</param>
|
/// <param name="userTimeSeconds">Current user time (не используется, порт сигнатуры).</param>
|
||||||
public CubismMotionQueueEntry StartMotion(ACubismMotion motion, bool autoDelete, float userTimeSeconds)
|
public CubismMotionQueueEntry StartMotion(ACubismMotion motion, bool autoDelete, float userTimeSeconds)
|
||||||
{
|
{
|
||||||
if (motion == null)
|
if (motion == null)
|
||||||
return 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);
|
var entry = new CubismMotionQueueEntry(motion, autoDelete);
|
||||||
motion.SetupMotionQueueEntry(entry, userTimeSeconds);
|
|
||||||
_entries.Add(entry);
|
_entries.Add(entry);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
@@ -123,40 +191,68 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
public bool IsFinished()
|
public bool IsFinished()
|
||||||
{
|
{
|
||||||
foreach (var entry in _entries)
|
foreach (var entry in _entries)
|
||||||
if (!entry.IsFinished)
|
if (entry != null && !entry.IsFinished)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-frame update for all queued motions.
|
/// Per-frame update — порт CubismMotionQueueManager::DoUpdateMotion.
|
||||||
/// Iterates in reverse for safe removal. For each active entry:
|
/// Для каждой записи: UpdateParameters (фейд + применение значений + финализация
|
||||||
/// computes fade weight, invokes <see cref="ACubismMotion.DoUpdateParameters"/>,
|
/// IsFinished самим моушеном), сборUserData-событий за кадр, затем удаление
|
||||||
/// and checks for completion. Finished auto-delete entries are removed.
|
/// finished-записей и старт отложенного фейда-аута вытесненных.
|
||||||
|
/// Обход прямой, как в SDK: моушены применяются от старых к новым.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void DoUpdateMotion(CubismNativeModel model, float userTimeSeconds)
|
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];
|
CubismMotionQueueEntry entry = _entries[i];
|
||||||
if (!entry.IsAvailable || entry.IsFinished)
|
|
||||||
|
if (entry == null)
|
||||||
{
|
{
|
||||||
if (entry.AutoDelete)
|
_entries.RemoveAt(i);
|
||||||
_entries.RemoveAt(i);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var weight = entry.Motion.UpdateFadeWeight(entry, userTimeSeconds);
|
ACubismMotion motion = entry.Motion;
|
||||||
entry.Motion.DoUpdateParameters(model, userTimeSeconds, weight, entry);
|
if (motion == null)
|
||||||
|
|
||||||
if (entry.Motion.GetDuration() > 0f)
|
|
||||||
{
|
{
|
||||||
if (userTimeSeconds >= entry.EndTime && !entry.Motion.IsLoop)
|
_entries.RemoveAt(i);
|
||||||
{
|
continue;
|
||||||
entry.IsFinished = true;
|
|
||||||
entry.Motion.OnFinishedMotion?.Invoke(entry.Motion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------ Применение значений (финализация — внутри моушена/обёртки) ------
|
||||||
|
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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,34 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
private GCHandle _mocAlignedHandle;
|
private GCHandle _mocAlignedHandle;
|
||||||
private GCHandle _modelAlignedHandle;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Routes native Core log output (the only channel for native-side errors:
|
||||||
|
/// invalid moc3, unsupported version, alignment failures) into GameLogger.
|
||||||
|
/// </summary>
|
||||||
|
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 _drawableCount;
|
||||||
private int _parameterCount;
|
private int _parameterCount;
|
||||||
|
|
||||||
@@ -67,38 +95,95 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe void LoadMoc(byte[] mocBytes)
|
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)
|
int alignedMocSize = (mocBytes.Length + CubismCoreBindings.AlignofMoc - 1)
|
||||||
/ CubismCoreBindings.AlignofMoc * CubismCoreBindings.AlignofMoc;
|
/ CubismCoreBindings.AlignofMoc * CubismCoreBindings.AlignofMoc;
|
||||||
byte[] mocBuffer = new byte[alignedMocSize + CubismCoreBindings.AlignofMoc];
|
byte[] mocBuffer = new byte[alignedMocSize + CubismCoreBindings.AlignofMoc];
|
||||||
_mocAlignedHandle = GCHandle.Alloc(mocBuffer, GCHandleType.Pinned);
|
_mocAlignedHandle = GCHandle.Alloc(mocBuffer, GCHandleType.Pinned);
|
||||||
IntPtr mocPtr = _mocAlignedHandle.AddrOfPinnedObject();
|
try
|
||||||
long alignedAddr = (mocPtr.ToInt64() + CubismCoreBindings.AlignofMoc - 1)
|
|
||||||
/ CubismCoreBindings.AlignofMoc * CubismCoreBindings.AlignofMoc;
|
|
||||||
var alignedMocPtr = new IntPtr(alignedAddr);
|
|
||||||
Marshal.Copy(mocBytes, 0, alignedMocPtr, mocBytes.Length);
|
|
||||||
|
|
||||||
IntPtr moc = CubismCoreBindings.csmReviveMocInPlace(
|
|
||||||
(void*)alignedMocPtr, (uint)mocBytes.Length);
|
|
||||||
if (moc == IntPtr.Zero)
|
|
||||||
{
|
{
|
||||||
GameLogger.Log("LIVE2D", "LoadMoc: csmReviveMocInPlace failed — .moc3 data is invalid or corrupted", LogLevel.Error);
|
IntPtr mocPtr = _mocAlignedHandle.AddrOfPinnedObject();
|
||||||
throw new InvalidOperationException("csmReviveMocInPlace failed");
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
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);
|
// Any failure after the Alloc calls must release the pinned buffers —
|
||||||
throw new InvalidOperationException("csmInitializeModelInPlace failed");
|
// 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);
|
var sizeInPixels = default(CubismCoreBindings.csmVector2);
|
||||||
@@ -319,6 +404,39 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
public unsafe IntPtr GetPartIds() =>
|
public unsafe IntPtr GetPartIds() =>
|
||||||
CubismCoreBindings.csmGetPartIds(_model);
|
CubismCoreBindings.csmGetPartIds(_model);
|
||||||
|
|
||||||
|
// Snapshot of the parameter values for the Load/Save bracket
|
||||||
|
// (port of CubismModel.cpp:878-910).
|
||||||
|
private float[] _savedParameterValues;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Порт CubismModel::SaveParameters: сохраняет текущие значения параметров в снимок.
|
||||||
|
/// Вызывается сразу после фазы моушена, чтобы аддитивные слои (выражения, дыхание,
|
||||||
|
/// look-инъекция) каждый кадр стартовали от одного и того же базового состояния
|
||||||
|
/// и не накапливались кадр-за-кадром.
|
||||||
|
/// </summary>
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Порт CubismModel::LoadParameters: восстанавливает значения параметров из снимка.
|
||||||
|
/// До первого SaveParameters ничего не делает (как в SDK).
|
||||||
|
/// </summary>
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Frees pinned memory handles. The native model handle is set to zero;
|
/// Frees pinned memory handles. The native model handle is set to zero;
|
||||||
/// the underlying CubismCore memory was allocated inline and lives in
|
/// the underlying CubismCore memory was allocated inline and lives in
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A single particle in a physics chain. Holds position, velocity, force accumulators,
|
/// A single particle in a physics chain. Holds position, velocity, force accumulators,
|
||||||
/// and per-particle properties (mobility, delay, acceleration, radius) from the
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct CubismPhysicsParticle
|
public struct CubismPhysicsParticle
|
||||||
{
|
{
|
||||||
@@ -61,8 +60,11 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps a physics chain output back to a model parameter.
|
/// Maps a physics chain output back to a model parameter.
|
||||||
/// Defines which particle vertex to read, the output type (X, Y, angle),
|
/// <see cref="VertexIndex"/> is the pendulum vertex index relative to the
|
||||||
/// translation scale, weight, and reflection.
|
/// owning sub-rig's base particle (SDK semantics). The translation scale is
|
||||||
|
/// deliberately never loaded: native CubismPhysicsJson reads only the
|
||||||
|
/// single "Scale" value into <see cref="AngleScale"/>, so X/Y-typed outputs
|
||||||
|
/// scale by zero exactly like the native implementation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct CubismPhysicsOutput
|
public struct CubismPhysicsOutput
|
||||||
{
|
{
|
||||||
@@ -95,146 +97,251 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// JSON parser for the Cubism .physics3.json format.
|
/// JSON parser for the Cubism .physics3.json format. Field-by-field port of
|
||||||
/// Reads the Meta section (fps, gravity, wind) and the PhysicsSettings
|
/// Cubism SDK for Native 5-r.5 Physics/CubismPhysicsJson.cpp: every getter
|
||||||
/// array (sub-rigs with their inputs, outputs, and vertices/particles).
|
/// reads the same key at the same JSON path. Missing numeric values yield
|
||||||
///
|
/// 0 / false like the native error-value accessors; in particular a
|
||||||
/// All data is accessed through typed out-parameter methods that
|
/// missing Meta.Fps yields 0 (the simulation then steps per display frame).
|
||||||
/// parse JSON elements directly — avoiding intermediate allocation
|
/// Note: native does not read InitialPosition/InitialVelocity for vertices —
|
||||||
/// of temporary data structures.
|
/// those keys are ignored here as well; particles are laid out by
|
||||||
|
/// CubismPhysics.Initialize().
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CubismPhysicsJson
|
public class CubismPhysicsJson
|
||||||
{
|
{
|
||||||
private readonly JsonDocument _json;
|
private readonly JsonDocument _json;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Parses the given .physics3.json bytes.</summary>
|
||||||
/// Parses the given .physics3.json bytes.
|
|
||||||
/// </summary>
|
|
||||||
public CubismPhysicsJson(byte[] jsonBytes)
|
public CubismPhysicsJson(byte[] jsonBytes)
|
||||||
{
|
{
|
||||||
_json = JsonDocument.Parse(jsonBytes);
|
_json = JsonDocument.Parse(jsonBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Physics simulation FPS from the Meta section, default 30.</summary>
|
// ------------------------------------------------------------------
|
||||||
public float GetFps()
|
// Meta
|
||||||
{
|
// ------------------------------------------------------------------
|
||||||
var meta = _json.RootElement.GetProperty("Meta");
|
|
||||||
return meta.TryGetProperty("Fps", out var v) ? v.GetSingle() : 30f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Number of sub-rig entries in PhysicsSettings.</summary>
|
/// <summary>Gravity vector from Meta.EffectiveForces.Gravity (missing values read as 0).</summary>
|
||||||
public int GetSubRigCount() =>
|
|
||||||
_json.RootElement.TryGetProperty("PhysicsSettings", out var arr) ? arr.GetArrayLength() : 0;
|
|
||||||
|
|
||||||
/// <summary>Reads gravity vector from Meta.EffectiveForces.Gravity. Defaults to (0, -1).</summary>
|
|
||||||
public void GetGravity(out float x, out float y)
|
public void GetGravity(out float x, out float y)
|
||||||
{
|
{
|
||||||
x = 0; y = -1;
|
x = 0f;
|
||||||
var meta = _json.RootElement.GetProperty("Meta");
|
y = 0f;
|
||||||
if (meta.TryGetProperty("EffectiveForces", out var ef) &&
|
if (_json.RootElement.TryGetProperty("Meta", out var meta) &&
|
||||||
ef.TryGetProperty("Gravity", out var g))
|
meta.TryGetProperty("EffectiveForces", out var effectiveForces) &&
|
||||||
|
effectiveForces.TryGetProperty("Gravity", out var gravity))
|
||||||
{
|
{
|
||||||
x = g.TryGetProperty("X", out var gx) ? gx.GetSingle() : 0f;
|
x = GetFloat(gravity, "X");
|
||||||
y = g.TryGetProperty("Y", out var gy) ? gy.GetSingle() : -1f;
|
y = GetFloat(gravity, "Y");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reads wind vector from Meta.EffectiveForces.Wind. Defaults to (0, 0).</summary>
|
/// <summary>Wind vector from Meta.EffectiveForces.Wind (missing values read as 0).</summary>
|
||||||
public void GetWind(out float x, out float y)
|
public void GetWind(out float x, out float y)
|
||||||
{
|
{
|
||||||
x = 0; y = 0;
|
x = 0f;
|
||||||
var meta = _json.RootElement.GetProperty("Meta");
|
y = 0f;
|
||||||
if (meta.TryGetProperty("EffectiveForces", out var ef) &&
|
if (_json.RootElement.TryGetProperty("Meta", out var meta) &&
|
||||||
ef.TryGetProperty("Wind", out var w))
|
meta.TryGetProperty("EffectiveForces", out var effectiveForces) &&
|
||||||
|
effectiveForces.TryGetProperty("Wind", out var wind))
|
||||||
{
|
{
|
||||||
x = w.TryGetProperty("X", out var wx) ? wx.GetSingle() : 0f;
|
x = GetFloat(wind, "X");
|
||||||
y = w.TryGetProperty("Y", out var wy) ? wy.GetSingle() : 0f;
|
y = GetFloat(wind, "Y");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Number of input entries in the given sub-rig.</summary>
|
/// <summary>Simulation FPS from Meta.Fps; 0 when absent (native behavior).</summary>
|
||||||
public int GetInputCount(int subRigIndex) =>
|
public float GetFps()
|
||||||
GetSubRig(subRigIndex).GetProperty("Input").GetArrayLength();
|
{
|
||||||
|
return _json.RootElement.TryGetProperty("Meta", out var meta) ? GetFloat(meta, "Fps") : 0f;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Number of output entries in the given sub-rig.</summary>
|
/// <summary>Sub-rig count from Meta.PhysicsSettingCount.</summary>
|
||||||
public int GetOutputCount(int subRigIndex) =>
|
public int GetSubRigCount()
|
||||||
GetSubRig(subRigIndex).GetProperty("Output").GetArrayLength();
|
{
|
||||||
|
if (_json.RootElement.TryGetProperty("Meta", out var meta) &&
|
||||||
|
meta.TryGetProperty("PhysicsSettingCount", out var count))
|
||||||
|
{
|
||||||
|
return count.GetInt32();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Number of vertex/particle entries in the given sub-rig.</summary>
|
// Defensive fallback: use the actual settings array length
|
||||||
public int GetVertexCount(int subRigIndex) =>
|
// (native would return 0 when the key is absent).
|
||||||
GetSubRig(subRigIndex).GetProperty("Vertices").GetArrayLength();
|
return _json.RootElement.TryGetProperty("PhysicsSettings", out var settings)
|
||||||
|
? settings.GetArrayLength()
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Reads position normalization (min, max, default) for the given sub-rig.</summary>
|
/// <summary>Total input count from Meta.TotalInputCount.</summary>
|
||||||
|
public int GetTotalInputCount() => GetMetaInt("TotalInputCount");
|
||||||
|
|
||||||
|
/// <summary>Total output count from Meta.TotalOutputCount.</summary>
|
||||||
|
public int GetTotalOutputCount() => GetMetaInt("TotalOutputCount");
|
||||||
|
|
||||||
|
/// <summary>Total vertex/particle count from Meta.VertexCount.</summary>
|
||||||
|
public int GetVertexCount() => GetMetaInt("VertexCount");
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Normalization
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Position normalization (Minimum/Maximum/Default) for a sub-rig; missing values read as 0.</summary>
|
||||||
public void GetNormalizationPosition(int subRigIndex, out CubismPhysicsNormalization norm)
|
public void GetNormalizationPosition(int subRigIndex, out CubismPhysicsNormalization norm)
|
||||||
{
|
{
|
||||||
norm = default;
|
norm = default;
|
||||||
var sr = GetSubRig(subRigIndex);
|
if (GetSubRig(subRigIndex).TryGetProperty("Normalization", out var n) &&
|
||||||
if (!sr.TryGetProperty("Normalization", out var n)) return;
|
n.TryGetProperty("Position", out var pos))
|
||||||
if (!n.TryGetProperty("Position", out var pos)) return;
|
{
|
||||||
norm.Minimum = pos.TryGetProperty("Minimum", out var p) ? p.GetSingle() : -30f;
|
norm.Minimum = GetFloat(pos, "Minimum");
|
||||||
norm.Maximum = pos.TryGetProperty("Maximum", out var pp) ? pp.GetSingle() : 30f;
|
norm.Maximum = GetFloat(pos, "Maximum");
|
||||||
norm.Default = pos.TryGetProperty("Default", out var pd) ? pd.GetSingle() : 0f;
|
norm.Default = GetFloat(pos, "Default");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reads angle normalization (min, max, default) for the given sub-rig.</summary>
|
/// <summary>Angle normalization (Minimum/Maximum/Default) for a sub-rig; missing values read as 0.</summary>
|
||||||
public void GetNormalizationAngle(int subRigIndex, out CubismPhysicsNormalization norm)
|
public void GetNormalizationAngle(int subRigIndex, out CubismPhysicsNormalization norm)
|
||||||
{
|
{
|
||||||
norm = default;
|
norm = default;
|
||||||
var sr = GetSubRig(subRigIndex);
|
if (GetSubRig(subRigIndex).TryGetProperty("Normalization", out var n) &&
|
||||||
if (!sr.TryGetProperty("Normalization", out var n)) return;
|
n.TryGetProperty("Angle", out var angle))
|
||||||
if (!n.TryGetProperty("Angle", out var angle)) return;
|
{
|
||||||
norm.Minimum = angle.TryGetProperty("Minimum", out var p) ? p.GetSingle() : -30f;
|
norm.Minimum = GetFloat(angle, "Minimum");
|
||||||
norm.Maximum = angle.TryGetProperty("Maximum", out var pp) ? pp.GetSingle() : 30f;
|
norm.Maximum = GetFloat(angle, "Maximum");
|
||||||
norm.Default = angle.TryGetProperty("Default", out var pd) ? pd.GetSingle() : 0f;
|
norm.Default = GetFloat(angle, "Default");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reads a single input entry: parameter ID, weight, type, reflect flag.</summary>
|
// ------------------------------------------------------------------
|
||||||
public void GetInput(int subRigIndex, int inputIndex, out string parameterId, out float weight,
|
// Input
|
||||||
out CubismPhysicsSource type, out bool reflect)
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Number of input entries in the given sub-rig.</summary>
|
||||||
|
public int GetInputCount(int subRigIndex) => GetEntriesLength(subRigIndex, "Input");
|
||||||
|
|
||||||
|
/// <summary>Input weight (Input[i].Weight).</summary>
|
||||||
|
public float GetInputWeight(int subRigIndex, int inputIndex) =>
|
||||||
|
GetFloat(GetInputEntry(subRigIndex, inputIndex), "Weight");
|
||||||
|
|
||||||
|
/// <summary>Input reflect flag (Input[i].Reflect).</summary>
|
||||||
|
public bool GetInputReflect(int subRigIndex, int inputIndex) =>
|
||||||
|
GetBool(GetInputEntry(subRigIndex, inputIndex), "Reflect");
|
||||||
|
|
||||||
|
/// <summary>Input type tag (Input[i].Type): "X", "Y" or "Angle".</summary>
|
||||||
|
public string GetInputType(int subRigIndex, int inputIndex) =>
|
||||||
|
GetString(GetInputEntry(subRigIndex, inputIndex), "Type");
|
||||||
|
|
||||||
|
/// <summary>Source parameter id (Input[i].Source.Id).</summary>
|
||||||
|
public string GetInputSourceId(int subRigIndex, int inputIndex)
|
||||||
{
|
{
|
||||||
var inp = GetSubRig(subRigIndex).GetProperty("Input")[inputIndex];
|
var entry = GetInputEntry(subRigIndex, inputIndex);
|
||||||
parameterId = inp.GetProperty("Source").GetProperty("Id").GetString();
|
return entry.TryGetProperty("Source", out var source) ? GetString(source, "Id") : null;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reads a single output entry: destination parameter ID, vertex index, scales, type, reflect.</summary>
|
// ------------------------------------------------------------------
|
||||||
public void GetOutput(int subRigIndex, int outputIndex, out string parameterId, out int vertexIndex,
|
// Output
|
||||||
out float scaleX, out float scaleY, out float weight, out CubismPhysicsSource type, out bool reflect)
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Number of output entries in the given sub-rig.</summary>
|
||||||
|
public int GetOutputCount(int subRigIndex) => GetEntriesLength(subRigIndex, "Output");
|
||||||
|
|
||||||
|
/// <summary>Pendulum vertex index (Output[i].VertexIndex), relative to the sub-rig base particle.</summary>
|
||||||
|
public int GetOutputVertexIndex(int subRigIndex, int outputIndex) =>
|
||||||
|
GetInt(GetOutputEntry(subRigIndex, outputIndex), "VertexIndex");
|
||||||
|
|
||||||
|
/// <summary>Angle scale (Output[i].Scale). Native maps the single "Scale" value to the angle scale only.</summary>
|
||||||
|
public float GetOutputAngleScale(int subRigIndex, int outputIndex) =>
|
||||||
|
GetFloat(GetOutputEntry(subRigIndex, outputIndex), "Scale");
|
||||||
|
|
||||||
|
/// <summary>Output weight (Output[i].Weight).</summary>
|
||||||
|
public float GetOutputWeight(int subRigIndex, int outputIndex) =>
|
||||||
|
GetFloat(GetOutputEntry(subRigIndex, outputIndex), "Weight");
|
||||||
|
|
||||||
|
/// <summary>Destination parameter id (Output[i].Destination.Id).</summary>
|
||||||
|
public string GetOutputsDestinationId(int subRigIndex, int outputIndex)
|
||||||
{
|
{
|
||||||
var outp = GetSubRig(subRigIndex).GetProperty("Output")[outputIndex];
|
var entry = GetOutputEntry(subRigIndex, outputIndex);
|
||||||
parameterId = outp.GetProperty("Destination").GetProperty("Id").GetString();
|
return entry.TryGetProperty("Destination", out var destination) ? GetString(destination, "Id") : null;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reads a single particle/vertex: mobility, delay, acceleration, radius, position.</summary>
|
/// <summary>Output type tag (Output[i].Type): "X", "Y" or "Angle".</summary>
|
||||||
public void GetParticle(int subRigIndex, int vertexIndex,
|
public string GetOutputType(int subRigIndex, int outputIndex) =>
|
||||||
out float mobility, out float delay, out float acceleration, out float radius,
|
GetString(GetOutputEntry(subRigIndex, outputIndex), "Type");
|
||||||
out float posX, out float posY)
|
|
||||||
|
/// <summary>Output reflect flag (Output[i].Reflect).</summary>
|
||||||
|
public bool GetOutputReflect(int subRigIndex, int outputIndex) =>
|
||||||
|
GetBool(GetOutputEntry(subRigIndex, outputIndex), "Reflect");
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Particle (Vertices)
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Number of vertex/particle entries in the given sub-rig.</summary>
|
||||||
|
public int GetParticleCount(int subRigIndex) => GetEntriesLength(subRigIndex, "Vertices");
|
||||||
|
|
||||||
|
/// <summary>Particle mobility (Vertices[i].Mobility).</summary>
|
||||||
|
public float GetParticleMobility(int subRigIndex, int vertexIndex) =>
|
||||||
|
GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Mobility");
|
||||||
|
|
||||||
|
/// <summary>Particle delay (Vertices[i].Delay).</summary>
|
||||||
|
public float GetParticleDelay(int subRigIndex, int vertexIndex) =>
|
||||||
|
GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Delay");
|
||||||
|
|
||||||
|
/// <summary>Particle acceleration (Vertices[i].Acceleration).</summary>
|
||||||
|
public float GetParticleAcceleration(int subRigIndex, int vertexIndex) =>
|
||||||
|
GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Acceleration");
|
||||||
|
|
||||||
|
/// <summary>Particle radius (Vertices[i].Radius).</summary>
|
||||||
|
public float GetParticleRadius(int subRigIndex, int vertexIndex) =>
|
||||||
|
GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Radius");
|
||||||
|
|
||||||
|
/// <summary>Particle position (Vertices[i].Position.X/Y).</summary>
|
||||||
|
public void GetParticlePosition(int subRigIndex, int vertexIndex, out float x, out float y)
|
||||||
{
|
{
|
||||||
var verts = GetSubRig(subRigIndex).GetProperty("Vertices")[vertexIndex];
|
var entry = GetVertexEntry(subRigIndex, vertexIndex);
|
||||||
mobility = verts.TryGetProperty("Mobility", out var m) ? m.GetSingle() : 1f;
|
x = 0f;
|
||||||
delay = verts.TryGetProperty("Delay", out var d) ? d.GetSingle() : 0f;
|
y = 0f;
|
||||||
acceleration = verts.TryGetProperty("Acceleration", out var a) ? a.GetSingle() : 1f;
|
if (entry.TryGetProperty("Position", out var position))
|
||||||
radius = verts.TryGetProperty("Radius", out var r) ? r.GetSingle() : 1f;
|
{
|
||||||
var pos = verts.GetProperty("Position");
|
x = GetFloat(position, "X");
|
||||||
posX = pos.TryGetProperty("X", out var px) ? px.GetSingle() : 0f;
|
y = GetFloat(position, "Y");
|
||||||
posY = pos.TryGetProperty("Y", out var py) ? py.GetSingle() : 0f;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
private JsonElement GetSubRig(int index) =>
|
private JsonElement GetSubRig(int index) =>
|
||||||
_json.RootElement.GetProperty("PhysicsSettings")[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,
|
var subRig = GetSubRig(subRigIndex);
|
||||||
"Y" => CubismPhysicsSource.Y,
|
return subRig.TryGetProperty(name, out var entries) ? entries.GetArrayLength() : 0;
|
||||||
_ => CubismPhysicsSource.X
|
}
|
||||||
};
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,44 +6,75 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages pose-based part visibility from a Cubism .pose3.json file.
|
/// Manages pose-based part visibility from a Cubism .pose3.json file.
|
||||||
/// Poses organize model parts into mutual-exclusion groups — only one
|
/// Pose groups organize model parts into mutual-exclusion groups — only one
|
||||||
/// part in each group is visible at a time. This is commonly used for
|
/// part in each group is visible at a time. Commonly used for eye states
|
||||||
/// eye states (open/closed/surprised) and other binary/multi-state
|
/// (open/closed/surprised) and other multi-state part switches.
|
||||||
/// part switches.
|
///
|
||||||
///
|
/// This is a faithful port of CubismPose.cpp (Cubism SDK for Native 5-r.5):
|
||||||
/// How it works:
|
|
||||||
/// <list type="number">
|
/// <list type="number">
|
||||||
/// <item>On model change (detected via ModelPtr comparison), all
|
/// <item>Part groups are stored as a flattened list with per-group counts
|
||||||
/// part groups are reset: the first part in each group gets
|
/// (matching the SDK layout, so index arithmetic in Reset/DoFade is identical).</item>
|
||||||
/// opacity 1, others get opacity 0</item>
|
/// <item>Each group part references both a part index (for opacity) and a
|
||||||
/// <item>Each frame, the currently visible part in each group
|
/// parameter index (for visibility). The visible part of a group is
|
||||||
/// fades toward opacity 1 while all others fade toward 0</item>
|
/// decided by the PARAMETER VALUE (> Epsilon), not by the part opacity.
|
||||||
/// <item>Linked parts (defined by the "Link" array in pose data)
|
/// If a pose parameter/part does not exist in the model, the SDK's
|
||||||
/// copy their parent's opacity, cascading visibility changes</item>
|
/// "not exist" maps are emulated, and the group falls back to its
|
||||||
|
/// first part with opacity 1.</item>
|
||||||
|
/// <item>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.</item>
|
||||||
|
/// <item>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.</item>
|
||||||
|
/// <item>CopyPartOpacities: parts with a "Link" list copy their current
|
||||||
|
/// part opacity to every linked part.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CubismPose
|
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 string PartId;
|
||||||
public int PartIndex;
|
public int PartIndex = -1;
|
||||||
public int ParameterIndex;
|
public int ParameterIndex = -1;
|
||||||
public List<PartData> Link;
|
public readonly List<PartData> Link = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SDK CubismPose::PartData::Initialize — resolves both indices for
|
||||||
|
/// this part id and initializes its pose parameter to 1.
|
||||||
|
/// </summary>
|
||||||
|
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<List<PartData>> _partGroups = new();
|
private readonly List<PartData> _partGroups = new();
|
||||||
private readonly float _fadeTimeSeconds;
|
private readonly List<int> _partGroupCounts = new();
|
||||||
|
private float _fadeTimeSeconds = DefaultFadeInSeconds;
|
||||||
private IntPtr _lastModel = IntPtr.Zero;
|
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<string, float> _notExistParameterValues = new();
|
||||||
|
private readonly Dictionary<string, float> _notExistPartOpacities = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses a .pose3.json byte buffer.
|
/// Parses a .pose3.json byte buffer.
|
||||||
/// Extracts the global fade time and all part groups with their
|
/// Extracts the fade time and all part groups (flattened, with counts per
|
||||||
/// linked-part chains.
|
/// group) including their linked-part chains.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CubismPose(byte[] pose3JsonBytes)
|
public CubismPose(byte[] pose3JsonBytes)
|
||||||
{
|
{
|
||||||
@@ -51,168 +82,273 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
|
|
||||||
var json = JsonDocument.Parse(pose3JsonBytes);
|
var json = JsonDocument.Parse(pose3JsonBytes);
|
||||||
var root = json.RootElement;
|
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())
|
foreach (var group in groups.EnumerateArray())
|
||||||
{
|
{
|
||||||
var partList = new List<PartData>();
|
var groupCount = 0;
|
||||||
foreach (var part in group.EnumerateArray())
|
foreach (var part in group.EnumerateArray())
|
||||||
{
|
{
|
||||||
var pd = new PartData
|
var pd = new PartData
|
||||||
{
|
{
|
||||||
PartId = part.GetProperty("Id").GetString(),
|
PartId = part.GetProperty("Id").GetString()
|
||||||
PartIndex = -1,
|
|
||||||
ParameterIndex = -1,
|
|
||||||
Link = new List<PartData>()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (part.TryGetProperty("Link", out var links))
|
if (part.TryGetProperty("Link", out var links) && links.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
foreach (var link in links.EnumerateArray())
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-frame pose update. Detects model pointer changes to trigger
|
/// Per-frame pose update, mirroring CubismPose::UpdateParameters.
|
||||||
/// a full reset, then fades parts within each group and propagates
|
/// Detects model changes to trigger a full reset, then fades each group
|
||||||
/// opacities to linked parts.
|
/// and propagates opacities to linked parts.
|
||||||
/// Called from <see cref="CubismModelNode.OnUpdate"/> after expressions.
|
/// Called from <see cref="CubismModelNode.OnUpdate"/> after expressions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe void UpdateParameters(CubismNativeModel model, float deltaTimeSeconds)
|
public void UpdateParameters(CubismNativeModel model, float deltaTimeSeconds)
|
||||||
{
|
{
|
||||||
if (model == null || model.ModelPtr == IntPtr.Zero) return;
|
if (model == null || model.ModelPtr == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
// 前回のモデルと同じではないときは初期化が必要
|
||||||
if (_lastModel != model.ModelPtr)
|
if (_lastModel != model.ModelPtr)
|
||||||
{
|
|
||||||
_lastModel = model.ModelPtr;
|
|
||||||
Reset(model);
|
Reset(model);
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var group in _partGroups)
|
_lastModel = model.ModelPtr;
|
||||||
DoFade(model, group, deltaTimeSeconds);
|
|
||||||
|
// 設定から時間を変更すると、経過時間がマイナスになることがあるので、経過時間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);
|
CopyPartOpacities(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>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.</summary>
|
||||||
private unsafe void Reset(CubismNativeModel model)
|
private unsafe void Reset(CubismNativeModel model)
|
||||||
{
|
{
|
||||||
var idsPtr = model.GetPartIds();
|
var beginIndex = 0;
|
||||||
var opacities = model.GetPartOpacities();
|
|
||||||
int partCount = model.GetPartCount();
|
|
||||||
|
|
||||||
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];
|
var pd = _partGroups[j];
|
||||||
pd.PartIndex = -1;
|
pd.Initialize(model, this);
|
||||||
pd.ParameterIndex = -1;
|
|
||||||
|
|
||||||
for (int pi = 0; pi < partCount; pi++)
|
if (pd.PartIndex < 0)
|
||||||
{
|
continue;
|
||||||
if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == pd.PartId)
|
|
||||||
{
|
|
||||||
pd.PartIndex = pi;
|
|
||||||
opacities[pi] = i == 0 ? 1f : 0f;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<PartData> group, float deltaTimeSeconds)
|
/// <summary>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.</summary>
|
||||||
|
private unsafe void DoFade(CubismNativeModel model, float deltaTimeSeconds, int beginIndex, int partGroupCount)
|
||||||
{
|
{
|
||||||
const float Phi = 0.5f;
|
var visiblePartIndex = -1;
|
||||||
const float BackOpacityThreshold = 0.15f;
|
var newOpacity = 1f;
|
||||||
|
|
||||||
var idsPtr = model.GetPartIds();
|
// 現在、表示状態になっているパーツを取得
|
||||||
var opacities = model.GetPartOpacities();
|
for (int i = beginIndex; i < beginIndex + partGroupCount; i++)
|
||||||
int partCount = model.GetPartCount();
|
|
||||||
|
|
||||||
float newOpacity = 1f;
|
|
||||||
int visibleIndex = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < group.Count; i++)
|
|
||||||
{
|
{
|
||||||
var pd = group[i];
|
var pd = _partGroups[i];
|
||||||
if (pd.PartIndex < 0) continue;
|
|
||||||
|
|
||||||
if (opacities[pd.PartIndex] > 0.001f)
|
if (GetParameterValue(model, pd.ParameterIndex, pd.PartId) > Epsilon)
|
||||||
{
|
{
|
||||||
visibleIndex = i;
|
if (visiblePartIndex >= 0)
|
||||||
break;
|
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];
|
visiblePartIndex = 0;
|
||||||
if (pd.PartIndex < 0) continue;
|
newOpacity = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
if (i == visibleIndex)
|
// 表示パーツ、非表示パーツの不透明度を設定する
|
||||||
|
for (int i = beginIndex; i < beginIndex + partGroupCount; i++)
|
||||||
|
{
|
||||||
|
var pd = _partGroups[i];
|
||||||
|
|
||||||
|
if (visiblePartIndex == i)
|
||||||
{
|
{
|
||||||
var fadeRate = deltaTimeSeconds / _fadeTimeSeconds;
|
SetPartOpacity(model, pd.PartIndex, pd.PartId, newOpacity); // 先に設定
|
||||||
newOpacity = MathF.Min(opacities[pd.PartIndex] + fadeRate, 1f);
|
|
||||||
opacities[pd.PartIndex] = newOpacity;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
float a1;
|
var opacity = GetPartOpacity(model, pd.PartIndex, pd.PartId);
|
||||||
if (newOpacity < Phi)
|
var a1 = ComputeHiddenOpacity(newOpacity);
|
||||||
a1 = newOpacity * (Phi - 1f) / Phi + 1f;
|
|
||||||
else
|
|
||||||
a1 = (1f - newOpacity) * Phi / (1f - Phi);
|
|
||||||
|
|
||||||
var backOpacity = (1f - a1) * (1f - newOpacity);
|
if (opacity > a1)
|
||||||
if (backOpacity > BackOpacityThreshold)
|
opacity = a1;
|
||||||
a1 = 1f - BackOpacityThreshold / (1f - newOpacity);
|
|
||||||
|
|
||||||
opacities[pd.PartIndex] = MathF.Min(opacities[pd.PartIndex], a1);
|
SetPartOpacity(model, pd.PartIndex, pd.PartId, opacity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>SDK CubismPose::CopyPartOpacities — every group part with a
|
||||||
|
/// Link list copies its current part opacity to all linked parts.</summary>
|
||||||
private unsafe void CopyPartOpacities(CubismNativeModel model)
|
private unsafe void CopyPartOpacities(CubismNativeModel model)
|
||||||
{
|
{
|
||||||
var idsPtr = model.GetPartIds();
|
foreach (var partData in _partGroups)
|
||||||
var opacities = model.GetPartOpacities();
|
|
||||||
int partCount = model.GetPartCount();
|
|
||||||
|
|
||||||
foreach (var group in _partGroups)
|
|
||||||
{
|
{
|
||||||
for (int gi = 0; gi < group.Count; gi++)
|
if (partData.Link.Count == 0)
|
||||||
{
|
continue; // 連動するパラメータはない
|
||||||
var pd = group[gi];
|
|
||||||
if (pd.PartIndex < 0 || pd.Link == null) continue;
|
|
||||||
|
|
||||||
var parentOpacity = opacities[pd.PartIndex];
|
var opacity = GetPartOpacity(model, partData.PartIndex, partData.PartId);
|
||||||
for (int li = 0; li < pd.Link.Count; li++)
|
|
||||||
{
|
for (int linkIndex = 0; linkIndex < partData.Link.Count; linkIndex++)
|
||||||
var link = pd.Link[li];
|
{
|
||||||
for (int pi = 0; pi < partCount; pi++)
|
var linkPart = partData.Link[linkIndex];
|
||||||
{
|
|
||||||
if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == link.PartId)
|
if (linkPart.PartIndex < 0)
|
||||||
{
|
continue;
|
||||||
link.PartIndex = pi;
|
|
||||||
opacities[pi] = parentOpacity;
|
SetPartOpacity(model, linkPart.PartIndex, linkPart.PartId, opacity);
|
||||||
pd.Link[li] = link;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Resolves a part id to a part index, or -1 when absent.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Resolves a parameter id to a parameter index, or -1 when absent.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure helper (SDK DoFade, visible branch): the visible part's opacity
|
||||||
|
/// grows by deltaTimeSeconds / fadeTimeSeconds, clamped to 1.
|
||||||
|
/// Internal for unit tests.
|
||||||
|
/// </summary>
|
||||||
|
internal static float ComputeVisibleOpacity(float currentOpacity, float deltaTimeSeconds, float fadeTimeSeconds)
|
||||||
|
{
|
||||||
|
var newOpacity = currentOpacity + (deltaTimeSeconds / fadeTimeSeconds);
|
||||||
|
return newOpacity > 1f ? 1f : newOpacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ using System.Buffers.Binary;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Cthangover.Core.Utils;
|
using Cthangover.Core.Utils;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
using Cthangover.Core.Mods.Resolvers;
|
||||||
using Cthangover.Live2D.Cubism.Core;
|
using Cthangover.Live2D.Cubism.Core;
|
||||||
|
|
||||||
namespace Cthangover.Live2D.Cubism.Framework
|
namespace Cthangover.Live2D.Cubism.Framework
|
||||||
@@ -187,9 +188,18 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] opacity={opacity:F3}");
|
GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] opacity={opacity:F3}");
|
||||||
|
|
||||||
var shader = ResolveShaderForDrawable(di, constantFlags, maskCounts);
|
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;
|
ShaderMaterial mat;
|
||||||
if (shader != null)
|
|
||||||
{
|
{
|
||||||
mat = new ShaderMaterial { Shader = shader };
|
mat = new ShaderMaterial { Shader = shader };
|
||||||
mat.SetShaderParameter("color_base", new Color(1f, 1f, 1f, 1f));
|
mat.SetShaderParameter("color_base", new Color(1f, 1f, 1f, 1f));
|
||||||
@@ -221,15 +231,8 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
mi.Material = mat;
|
mi.Material = mat;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (texture != null)
|
if (texture == null)
|
||||||
{
|
GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] texture MISSING (texIndex={texIndex})", LogLevel.Warning);
|
||||||
mat.SetShaderParameter("tex_main", texture);
|
|
||||||
GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] texture Ok");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
GameLogger.Log("LIVE2D", $"BuildModel: drawable[{di}] texture MISSING (texIndex={texIndex})");
|
|
||||||
}
|
|
||||||
|
|
||||||
mi.Visible = true;
|
mi.Visible = true;
|
||||||
_parentNode.AddChild(mi);
|
_parentNode.AddChild(mi);
|
||||||
@@ -428,6 +431,8 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
var maskTexIndex = model.GetTextureIndices()[drawableIndex];
|
var maskTexIndex = model.GetTextureIndices()[drawableIndex];
|
||||||
var texture = maskTexIndex >= 0 && maskTexIndex < model.Textures.Length ? model.Textures[maskTexIndex] : null;
|
var texture = maskTexIndex >= 0 && maskTexIndex < model.Textures.Length ? model.Textures[maskTexIndex] : null;
|
||||||
|
|
||||||
|
mi2d.Mesh = mesh;
|
||||||
|
|
||||||
var shader = ResolveShader("2d_cubism_mask");
|
var shader = ResolveShader("2d_cubism_mask");
|
||||||
if (shader == null) return mesh;
|
if (shader == null) return mesh;
|
||||||
|
|
||||||
@@ -436,7 +441,6 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
if (texture != null)
|
if (texture != null)
|
||||||
mat.SetShaderParameter("tex_main", texture);
|
mat.SetShaderParameter("tex_main", texture);
|
||||||
|
|
||||||
mi2d.Mesh = mesh;
|
|
||||||
mi2d.Material = mat;
|
mi2d.Material = mat;
|
||||||
return mesh;
|
return mesh;
|
||||||
}
|
}
|
||||||
@@ -478,14 +482,17 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
|
|
||||||
mi.ZIndex = renderOrders[di];
|
mi.ZIndex = renderOrders[di];
|
||||||
|
|
||||||
var mc = multiplyColors[di];
|
if (mat != null)
|
||||||
mat.SetShaderParameter("color_multiply", new Color(mc.X, mc.Y, mc.Z, mc.W));
|
{
|
||||||
|
var mc = multiplyColors[di];
|
||||||
|
mat.SetShaderParameter("color_multiply", new Color(mc.X, mc.Y, mc.Z, mc.W));
|
||||||
|
|
||||||
var sc = screenColors[di];
|
var sc = screenColors[di];
|
||||||
mat.SetShaderParameter("color_screen", new Color(sc.X, sc.Y, sc.Z, sc.W));
|
mat.SetShaderParameter("color_screen", new Color(sc.X, sc.Y, sc.Z, sc.W));
|
||||||
|
|
||||||
if ((flags & CubismCoreBindings.OpacityDidChange) != 0)
|
if ((flags & CubismCoreBindings.OpacityDidChange) != 0)
|
||||||
mat.SetShaderParameter("color_base", new Color(1f, 1f, 1f, opacities[di]));
|
mat.SetShaderParameter("color_base", new Color(1f, 1f, 1f, opacities[di]));
|
||||||
|
}
|
||||||
|
|
||||||
if ((flags & CubismCoreBindings.VertexPositionsDidChange) != 0)
|
if ((flags & CubismCoreBindings.VertexPositionsDidChange) != 0)
|
||||||
{
|
{
|
||||||
@@ -493,21 +500,103 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateMaskGroups(dynamicFlags, dc);
|
||||||
|
|
||||||
|
_model.ResetDynamicFlags();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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++)
|
for (int gi = 0; gi < _maskGroups.Count; gi++)
|
||||||
{
|
{
|
||||||
var group = _maskGroups[gi];
|
var group = _maskGroups[gi];
|
||||||
if (group.MaskDrawableIndices == null) continue;
|
var maskIndices = group.MaskDrawableIndices;
|
||||||
for (int mi = 0; mi < group.MaskDrawableIndices.Length; mi++)
|
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;
|
if (di < 0 || di >= dc) continue;
|
||||||
var mFlags = dynamicFlags[di];
|
var vCount = vertexCounts[di];
|
||||||
if ((mFlags & CubismCoreBindings.VertexPositionsDidChange) == 0) continue;
|
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);
|
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)
|
private unsafe void UpdateMeshVertices(int drawableIndex, ArrayMesh mesh)
|
||||||
@@ -594,7 +683,7 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
_drawableMaskChannel = null;
|
_drawableMaskChannel = null;
|
||||||
|
|
||||||
foreach (var group in _maskGroups)
|
foreach (var group in _maskGroups)
|
||||||
group.Viewport.QueueFree();
|
group.Viewport?.QueueFree();
|
||||||
_maskGroups.Clear();
|
_maskGroups.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,7 +720,9 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
{
|
{
|
||||||
try
|
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<ShaderResolver>()?.Resolve(name);
|
||||||
if (shader != null)
|
if (shader != null)
|
||||||
{
|
{
|
||||||
GameLogger.Log("LIVE2D", $"ResolveShader: '{name}' OK");
|
GameLogger.Log("LIVE2D", $"ResolveShader: '{name}' OK");
|
||||||
|
|||||||
@@ -6,21 +6,20 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
/// Smooth target-point tracking for face/eye movement parameters.
|
/// Smooth target-point tracking for face/eye movement parameters.
|
||||||
/// Drives the ParamAngleX/Y and ParamEyeBallX/Y model parameters in
|
/// Drives the ParamAngleX/Y and ParamEyeBallX/Y model parameters in
|
||||||
/// <see cref="CubismModelNode"/>.
|
/// <see cref="CubismModelNode"/>.
|
||||||
///
|
///
|
||||||
/// Uses a velocity-based smoothing approach: each update computes a
|
/// Bit-exact port of Cubism SDK for Native 5-r.5 Math/CubismTargetPoint.cpp:
|
||||||
/// desired velocity toward the target, applies acceleration limits,
|
/// a velocity-based follower that accelerates toward the target with a
|
||||||
/// enforces a maximum safe approach speed to prevent overshoot near
|
/// capped acceleration (reaching top speed in 0.15 s), then smoothly
|
||||||
/// the target, and integrates velocity into position.
|
/// decelerates so the configured acceleration can still brake before the
|
||||||
///
|
/// target. All time is expressed in internal 30 fps frame units.
|
||||||
/// 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).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CubismTargetPoint
|
public class CubismTargetPoint
|
||||||
{
|
{
|
||||||
private const float FaceParamMaxV = 40f;
|
/// <summary>Internal reference frame rate (SDK FrameRate).</summary>
|
||||||
private const float FrameToMaxSpeed = 4.5f;
|
private const int FrameRate = 30;
|
||||||
|
|
||||||
|
/// <summary>Threshold below which no movement is applied (SDK Epsilon).</summary>
|
||||||
|
private const float Epsilon = 0.01f;
|
||||||
|
|
||||||
private float _faceTargetX, _faceTargetY;
|
private float _faceTargetX, _faceTargetY;
|
||||||
private float _faceX, _faceY;
|
private float _faceX, _faceY;
|
||||||
@@ -45,54 +44,92 @@ namespace Cthangover.Live2D.Cubism.Framework
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Advances the smoothing simulation by one frame.
|
/// Advances the smoothing simulation by one frame (SDK CubismTargetPoint::Update).
|
||||||
/// Computes delta-time-weighted velocity toward target, applies
|
/// The very first call only anchors the internal clock and performs no
|
||||||
/// acceleration clamping and safe approach speed limiting, then
|
/// integration, matching the native implementation.
|
||||||
/// integrates position.
|
|
||||||
///
|
|
||||||
/// Called each frame from <see cref="CubismModelNode.OnUpdate"/>
|
|
||||||
/// before injecting face-tracking parameters.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Update(float deltaTimeSeconds)
|
public void Update(float deltaTimeSeconds)
|
||||||
{
|
{
|
||||||
|
// Add the delta time.
|
||||||
_userTimeSeconds += deltaTimeSeconds;
|
_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;
|
_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;
|
// The target direction is the vector (dx, dy).
|
||||||
var maxA = deltaTimeWeight * maxV / FrameToMaxSpeed;
|
float dx = _faceTargetX - _faceX;
|
||||||
|
float dy = _faceTargetY - _faceY;
|
||||||
|
|
||||||
var dx = _faceTargetX - _faceX;
|
if (MathF.Abs(dx) <= Epsilon && MathF.Abs(dy) <= Epsilon)
|
||||||
var dy = _faceTargetY - _faceY;
|
|
||||||
var distance = MathF.Sqrt(dx * dx + dy * dy);
|
|
||||||
if (distance < 0.01f) return;
|
|
||||||
|
|
||||||
var vx = maxV * dx / distance;
|
|
||||||
var vy = maxV * dy / distance;
|
|
||||||
|
|
||||||
var ax = vx - _faceVX;
|
|
||||||
var ay = vy - _faceVY;
|
|
||||||
var aLen = MathF.Sqrt(ax * ax + ay * ay);
|
|
||||||
if (aLen > 0f)
|
|
||||||
{
|
{
|
||||||
var clampedA = MathF.Min(aLen, maxA);
|
return; // No change.
|
||||||
ax = ax * clampedA / aLen;
|
|
||||||
ay = ay * clampedA / aLen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxSafeV = 0.5f * (MathF.Sqrt(maxA * maxA + 8f * maxA * distance) - maxA);
|
// Slow down when the distance requires more than the maximum speed.
|
||||||
var vLen = MathF.Sqrt(_faceVX * _faceVX + _faceVY * _faceVY);
|
float d = MathF.Sqrt((dx * dx) + (dy * dy));
|
||||||
if (vLen > maxSafeV && vLen > 0f)
|
|
||||||
|
// 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;
|
ax *= MaxA / a;
|
||||||
_faceVX *= scale;
|
ay *= MaxA / a;
|
||||||
_faceVY *= scale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the acceleration to the original velocity to get the new velocity.
|
||||||
_faceVX += ax;
|
_faceVX += ax;
|
||||||
_faceVY += ay;
|
_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;
|
_faceX += _faceVX;
|
||||||
_faceY += _faceVY;
|
_faceY += _faceVY;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using Cthangover.Live2D.Cubism.Framework;
|
using Cthangover.Live2D.Cubism.Framework;
|
||||||
|
|
||||||
namespace Cthangover.Live2D.Cubism.Services
|
namespace Cthangover.Live2D.Cubism.Services
|
||||||
@@ -19,6 +19,13 @@ namespace Cthangover.Live2D.Cubism.Services
|
|||||||
public float AnchorX = 0.5f;
|
public float AnchorX = 0.5f;
|
||||||
public float AnchorY = 0.5f;
|
public float AnchorY = 0.5f;
|
||||||
public FitMode ScaleFit = FitMode.FitAuto;
|
public FitMode ScaleFit = FitMode.FitAuto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableEyeBlink = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -68,7 +75,8 @@ namespace Cthangover.Live2D.Cubism.Services
|
|||||||
Name = config.NodeName,
|
Name = config.NodeName,
|
||||||
AnchorX = config.AnchorX,
|
AnchorX = config.AnchorX,
|
||||||
AnchorY = config.AnchorY,
|
AnchorY = config.AnchorY,
|
||||||
ScaleFit = fitMode
|
ScaleFit = fitMode,
|
||||||
|
EnableEyeBlink = config.EnableEyeBlink
|
||||||
};
|
};
|
||||||
|
|
||||||
parent.AddChild(model);
|
parent.AddChild(model);
|
||||||
|
|||||||
@@ -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")]
|
||||||
@@ -4,6 +4,7 @@ using Cthangover.Core.Utils;
|
|||||||
using Godot;
|
using Godot;
|
||||||
using Cthangover.Live2D.Cubism.Services;
|
using Cthangover.Live2D.Cubism.Services;
|
||||||
|
|
||||||
|
using Cthangover.Mods.Interface.View;
|
||||||
namespace Mods.Live2D
|
namespace Mods.Live2D
|
||||||
{
|
{
|
||||||
public class AddLive2DModelAction : IScenarioAction
|
public class AddLive2DModelAction : IScenarioAction
|
||||||
|
|||||||
Reference in new issue
Block a user