@airisu-dev/react-native-live2d-skia 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/Live2dSkia.podspec +159 -0
  3. package/README.md +64 -2
  4. package/android/CMakeLists.txt +113 -0
  5. package/android/build.gradle +223 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/live2dskia/Live2dSkiaPackage.kt +20 -0
  11. package/app.plugin.js +2 -0
  12. package/cli/build/index.js +157 -0
  13. package/cpp/cubism/CubismCompat.hpp +26 -0
  14. package/cpp/cubism/CubismHelpers.hpp +27 -0
  15. package/cpp/cubism/CubismRendererStub.cpp +14 -0
  16. package/cpp/cubism/CubismSetup.cpp +54 -0
  17. package/cpp/cubism/CubismSetup.hpp +9 -0
  18. package/cpp/cubism/HeldParameterUpdater.hpp +25 -0
  19. package/cpp/cubism/Live2DUserModel.cpp +125 -0
  20. package/cpp/cubism/Live2DUserModel.hpp +57 -0
  21. package/cpp/cubism/Live2DUserModelAnimation.cpp +107 -0
  22. package/cpp/drawing/JsiUnwrap.hpp +25 -0
  23. package/cpp/drawing/MeshCache.cpp +75 -0
  24. package/cpp/drawing/MeshCache.hpp +45 -0
  25. package/cpp/drawing/SkiaDrawer.cpp +138 -0
  26. package/cpp/drawing/SkiaDrawer.hpp +42 -0
  27. package/cpp/hybrid/HybridLive2DModel.cpp +174 -0
  28. package/cpp/hybrid/HybridLive2DModel.hpp +64 -0
  29. package/cpp/hybrid/HybridLive2dSkia.cpp +10 -0
  30. package/cpp/hybrid/HybridLive2dSkia.hpp +11 -0
  31. package/ios/Bridge.h +8 -0
  32. package/nitro.json +20 -0
  33. package/nitrogen/generated/.gitattributes +1 -0
  34. package/nitrogen/generated/android/Live2dSkia+autolinking.cmake +82 -0
  35. package/nitrogen/generated/android/Live2dSkia+autolinking.gradle +27 -0
  36. package/nitrogen/generated/android/Live2dSkiaOnLoad.cpp +49 -0
  37. package/nitrogen/generated/android/Live2dSkiaOnLoad.hpp +34 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/live2dskia/Live2dSkiaOnLoad.kt +35 -0
  39. package/nitrogen/generated/ios/Live2dSkia+autolinking.rb +62 -0
  40. package/nitrogen/generated/ios/Live2dSkia-Swift-Cxx-Bridge.cpp +17 -0
  41. package/nitrogen/generated/ios/Live2dSkia-Swift-Cxx-Bridge.hpp +27 -0
  42. package/nitrogen/generated/ios/Live2dSkia-Swift-Cxx-Umbrella.hpp +38 -0
  43. package/nitrogen/generated/ios/Live2dSkiaAutolinking.mm +35 -0
  44. package/nitrogen/generated/ios/Live2dSkiaAutolinking.swift +16 -0
  45. package/nitrogen/generated/shared/c++/DrawableData.hpp +116 -0
  46. package/nitrogen/generated/shared/c++/HybridLive2DModelSpec.cpp +39 -0
  47. package/nitrogen/generated/shared/c++/HybridLive2DModelSpec.hpp +86 -0
  48. package/nitrogen/generated/shared/c++/HybridLive2dSkiaSpec.cpp +21 -0
  49. package/nitrogen/generated/shared/c++/HybridLive2dSkiaSpec.hpp +66 -0
  50. package/nitrogen/generated/shared/c++/MotionPriority.hpp +80 -0
  51. package/package.json +146 -4
  52. package/plugin/build/index.d.ts +14 -0
  53. package/plugin/build/index.js +81 -0
  54. package/src/components/Live2D.tsx +131 -0
  55. package/src/components/index.ts +2 -0
  56. package/src/hooks/index.ts +8 -0
  57. package/src/hooks/useLive2DGesture.ts +140 -0
  58. package/src/hooks/useLive2DModel.ts +47 -0
  59. package/src/index.ts +5 -0
  60. package/src/loader/cubismJson.ts +23 -0
  61. package/src/loader/index.ts +9 -0
  62. package/src/loader/loadModel.ts +194 -0
  63. package/src/loader/modelContext.ts +80 -0
  64. package/src/loader/modelUrl.ts +112 -0
  65. package/src/native.ts +4 -0
  66. package/src/rendering/drawer.ts +256 -0
  67. package/src/rendering/skia.ts +114 -0
  68. package/src/rendering/ticker/addToTicker.ts +52 -0
  69. package/src/rendering/ticker/constants.ts +11 -0
  70. package/src/rendering/ticker/frames.ts +35 -0
  71. package/src/rendering/ticker/index.ts +5 -0
  72. package/src/rendering/ticker/loop.ts +115 -0
  73. package/src/rendering/ticker/state.ts +65 -0
  74. package/src/rendering/ticker/stats.ts +36 -0
  75. package/src/rendering/ticker/types.ts +57 -0
  76. package/src/specs/react-native-live2d-skia.nitro.ts +88 -0
  77. package/src/utils/clock.ts +10 -0
@@ -0,0 +1,20 @@
1
+ package com.live2dskia;
2
+
3
+ import com.facebook.react.bridge.NativeModule;
4
+ import com.facebook.react.bridge.ReactApplicationContext;
5
+ import com.facebook.react.module.model.ReactModuleInfoProvider;
6
+ import com.facebook.react.BaseReactPackage;
7
+ import com.margelo.nitro.live2dskia.Live2dSkiaOnLoad;
8
+
9
+
10
+ public class Live2dSkiaPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
14
+
15
+ companion object {
16
+ init {
17
+ Live2dSkiaOnLoad.initializeNative();
18
+ }
19
+ }
20
+ }
package/app.plugin.js ADDED
@@ -0,0 +1,2 @@
1
+ // Expo CLI looks for this file. The plugin is in plugin/src, built to plugin/build.
2
+ module.exports = require('./plugin/build').default
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.sha256 = void 0;
8
+ exports.parseArgs = parseArgs;
9
+ // @airisu-dev/react-native-live2d-skia fetch-sdk --accept-license [--sdk-version <version>] [--out <dir>] [--keep-zip]
10
+ //
11
+ // Downloads Cubism SDK for Native into <dir> (default ./vendor/CubismSdkForNative), for the
12
+ // config plugin's `cubismSdkPath`. The Core is proprietary and not shipped: downloading means accepting Live2D's license.
13
+ const child_process_1 = require("child_process");
14
+ const crypto_1 = __importDefault(require("crypto"));
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const path_1 = __importDefault(require("path"));
17
+ // The SDK versions the package supports. Two levels up from both cli/src and cli/build.
18
+ const { cubismSdk } = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'package.json'), 'utf8'));
19
+ const VERSIONS = Object.keys(cubismSdk.versions);
20
+ const sdkUrl = (version) => `https://cubism.live2d.com/sdk-native/bin/CubismSdkForNative-${version}.zip`;
21
+ const DEFAULT_OUT = 'vendor/CubismSdkForNative';
22
+ const LICENSES = [
23
+ 'Live2D Proprietary Software License (Cubism Core): https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html',
24
+ 'Live2D Open Software License (Cubism Native Framework): https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html',
25
+ 'Cubism SDK Release License (required to publish apps, for businesses above 10M JPY a year): https://www.live2d.com/en/sdk/license/',
26
+ ];
27
+ function usage() {
28
+ console.log(`Usage: @airisu-dev/react-native-live2d-skia fetch-sdk --accept-license [--sdk-version <version>] [--out <dir>] [--keep-zip]
29
+
30
+ Downloads Cubism SDK for Native from Live2D into <dir>
31
+ (default: ./${DEFAULT_OUT}). Then set it in app.json:
32
+
33
+ "plugins": [["@airisu-dev/react-native-live2d-skia", { "cubismSdkPath": "./${DEFAULT_OUT}" }]]
34
+
35
+ --accept-license Required: you accept Live2D's license terms:
36
+ ${LICENSES.map((l) => ` - ${l}`).join('\n')}
37
+ --sdk-version Which version: ${VERSIONS.join(', ')} (default: ${cubismSdk.latest}).
38
+ --out <dir> Where to extract the SDK.
39
+ --keep-zip Keep the downloaded zip next to <dir> (e.g. to cache it on CI).`);
40
+ }
41
+ function fail(message) {
42
+ console.error(`error: ${message}`);
43
+ process.exit(1);
44
+ }
45
+ function parseArgs(argv) {
46
+ const args = {
47
+ command: argv[0] === '--help' || argv[0] === '-h' ? 'help' : argv[0],
48
+ acceptLicense: false,
49
+ version: cubismSdk.latest,
50
+ out: DEFAULT_OUT,
51
+ keepZip: false,
52
+ };
53
+ for (let i = 1; i < argv.length; i++) {
54
+ const arg = argv[i];
55
+ const next = argv[i + 1];
56
+ if (arg === '--accept-license')
57
+ args.acceptLicense = true;
58
+ else if (arg === '--keep-zip')
59
+ args.keepZip = true;
60
+ else if (arg === '--sdk-version' && next) {
61
+ args.version = next;
62
+ i++;
63
+ }
64
+ else if (arg === '--out' && next) {
65
+ args.out = next;
66
+ i++;
67
+ }
68
+ else if (arg === '--help' || arg === '-h')
69
+ args.command = 'help';
70
+ else
71
+ throw new Error(`unknown option ${arg}`);
72
+ }
73
+ return args;
74
+ }
75
+ const sha256 = (file) => crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(file)).digest('hex');
76
+ exports.sha256 = sha256;
77
+ async function download(url, file) {
78
+ console.log(`Downloading ${url}`);
79
+ const response = await fetch(url);
80
+ if (!response.ok)
81
+ fail(`download failed: HTTP ${response.status}`);
82
+ fs_1.default.writeFileSync(file, Buffer.from(await response.arrayBuffer()));
83
+ }
84
+ // unzip where it exists (macOS, most Linux), else tar, which reads zips on
85
+ // Windows 10+ (bsdtar) and macOS.
86
+ function extract(zip, dir) {
87
+ const hasUnzip = !(0, child_process_1.spawnSync)('unzip', ['-v'], { stdio: 'ignore' }).error;
88
+ const [cmd, args] = hasUnzip
89
+ ? ['unzip', ['-q', zip, '-d', dir]]
90
+ : ['tar', ['-xf', zip, '-C', dir]];
91
+ const result = (0, child_process_1.spawnSync)(cmd, args, { stdio: 'inherit' });
92
+ if (result.error || result.status !== 0) {
93
+ fail(`couldn't extract ${zip} with ${cmd}`);
94
+ }
95
+ }
96
+ async function fetchSdk({ acceptLicense, version, out, keepZip }) {
97
+ if (!acceptLicense) {
98
+ console.error("Cubism SDK for Native is Live2D's software. Downloading it means accepting:\n" +
99
+ LICENSES.map((l) => ` - ${l}`).join('\n') +
100
+ '\nRe-run with --accept-license to accept them and download.');
101
+ process.exit(1);
102
+ }
103
+ const expected = cubismSdk.versions[version]?.sha256;
104
+ if (!expected)
105
+ fail(`Cubism SDK for Native ${version} isn't supported: use ${VERSIONS.join(', ')}`);
106
+ const sdkDir = path_1.default.resolve(out);
107
+ const versionFile = path_1.default.join(sdkDir, '.version');
108
+ if (fs_1.default.existsSync(versionFile) && fs_1.default.readFileSync(versionFile, 'utf8').trim() === version) {
109
+ console.log(`Cubism SDK for Native ${version} already in ${sdkDir}`);
110
+ return;
111
+ }
112
+ const zip = `${sdkDir}-${version}.zip`;
113
+ fs_1.default.mkdirSync(path_1.default.dirname(sdkDir), { recursive: true });
114
+ if (!fs_1.default.existsSync(zip) || (0, exports.sha256)(zip) !== expected) {
115
+ await download(sdkUrl(version), zip);
116
+ }
117
+ const actual = (0, exports.sha256)(zip);
118
+ if (actual !== expected) {
119
+ fs_1.default.rmSync(zip, { force: true });
120
+ fail(`checksum mismatch for ${zip} (expected ${expected}, got ${actual})`);
121
+ }
122
+ // Extract next to the destination, so the move is a rename on the same drive.
123
+ const tmp = `${sdkDir}.extracting`;
124
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
125
+ fs_1.default.mkdirSync(tmp);
126
+ try {
127
+ extract(zip, tmp);
128
+ fs_1.default.rmSync(sdkDir, { recursive: true, force: true });
129
+ fs_1.default.renameSync(path_1.default.join(tmp, `CubismSdkForNative-${version}`), sdkDir);
130
+ }
131
+ finally {
132
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
133
+ }
134
+ fs_1.default.writeFileSync(versionFile, `${version}\n`);
135
+ if (!keepZip)
136
+ fs_1.default.rmSync(zip, { force: true });
137
+ const relative = path_1.default.relative(process.cwd(), sdkDir).split(path_1.default.sep).join('/');
138
+ console.log(`Extracted Cubism SDK for Native ${version} to ${sdkDir}
139
+
140
+ Next:
141
+ - Keep it out of public repos: add "/${relative}/" to .gitignore.
142
+ - Set it in app.json: "plugins": [["@airisu-dev/react-native-live2d-skia", { "cubismSdkPath": "./${relative}" }]]
143
+ - On EAS Build, run this command in an "eas-build-post-install" script.`);
144
+ }
145
+ async function main() {
146
+ const args = parseArgs(process.argv.slice(2));
147
+ if (args.command === 'fetch-sdk')
148
+ await fetchSdk(args);
149
+ else {
150
+ usage();
151
+ if (args.command && args.command !== 'help')
152
+ process.exit(1);
153
+ }
154
+ }
155
+ if (require.main === module) {
156
+ main().catch((e) => fail(e instanceof Error ? e.message : String(e)));
157
+ }
@@ -0,0 +1,26 @@
1
+ #pragma once
2
+
3
+ // Differences between the Cubism SDK for Native versions react-native-live2d-skia builds
4
+ // against (cubismSdk.versions in package.json). build.gradle and the podspec
5
+ // pass the SDK's version as LIVE2D_SKIA_CUBISM_SDK: major * 10000 + release *
6
+ // 100 + patch, so 5-r.4.1 is 50401. Version-specific calls go here, behind
7
+ // #if on it (older SDK versions).
8
+
9
+ #include "Live2DCubismCore.hpp"
10
+
11
+ #ifndef LIVE2D_SKIA_CUBISM_SDK
12
+ #error "[react-native-live2d-skia] LIVE2D_SKIA_CUBISM_SDK isn't set: build through Gradle or CocoaPods."
13
+ #endif
14
+
15
+ #if LIVE2D_SKIA_CUBISM_SDK < 50500
16
+ #error "[react-native-live2d-skia] Cubism SDK for Native before 5-r.5 isn't supported."
17
+ #endif
18
+
19
+ namespace margelo::nitro::live2dskia::cubism {
20
+
21
+ // Drawable render orders. Named csmGetDrawableRenderOrders before 5-r.5.
22
+ inline const int* renderOrders(const Live2D::Cubism::Core::csmModel* model) {
23
+ return Live2D::Cubism::Core::csmGetRenderOrders(model);
24
+ }
25
+
26
+ } // namespace margelo::nitro::live2dskia::cubism
@@ -0,0 +1,27 @@
1
+ #pragma once
2
+ #include <stdexcept>
3
+ #include <string>
4
+ #include "CubismFramework.hpp"
5
+ #include "Id/CubismIdManager.hpp"
6
+ #include "Utils/CubismJson.hpp"
7
+
8
+ namespace margelo::nitro::live2dskia {
9
+
10
+ inline const Live2D::Cubism::Framework::csmByte* bytesOf(const std::string& s) {
11
+ return reinterpret_cast<const Live2D::Cubism::Framework::csmByte*>(s.data());
12
+ }
13
+
14
+ // The Framework's loaders don't check for parse errors and crash on invalid
15
+ // JSON, so parse it once up front and throw instead.
16
+ inline void requireValidJson(const std::string& json, const std::string& what) {
17
+ using namespace Live2D::Cubism::Framework;
18
+ Utils::CubismJson* parsed = Utils::CubismJson::Create(bytesOf(json), static_cast<csmSizeInt>(json.size()));
19
+ if (!parsed) throw std::runtime_error("Live2D: invalid " + what);
20
+ Utils::CubismJson::Delete(parsed);
21
+ }
22
+
23
+ inline Live2D::Cubism::Framework::CubismIdHandle idOf(const Live2D::Cubism::Framework::csmChar* name) {
24
+ return Live2D::Cubism::Framework::CubismFramework::GetIdManager()->GetId(name);
25
+ }
26
+
27
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,14 @@
1
+ // The Cubism Framework references a few renderer entry points that each
2
+ // Rendering/<backend> normally defines. We draw with Skia instead, so no
3
+ // backend is compiled and these are stubbed out.
4
+ #include "Rendering/CubismRenderer.hpp"
5
+
6
+ namespace Live2D::Cubism::Framework::Rendering {
7
+
8
+ CubismRenderer* CubismRenderer::Create(csmUint32, csmUint32) {
9
+ return nullptr;
10
+ }
11
+
12
+ void CubismRenderer::StaticRelease() {}
13
+
14
+ } // namespace Live2D::Cubism::Framework::Rendering
@@ -0,0 +1,54 @@
1
+ #include "CubismSetup.hpp"
2
+ #include <cstdio>
3
+ #include <cstdlib>
4
+ #include <mutex>
5
+ #include "CubismFramework.hpp"
6
+ #include "ICubismAllocator.hpp"
7
+
8
+ #ifdef __ANDROID__
9
+ #include <android/log.h>
10
+ #endif
11
+
12
+ namespace margelo::nitro::live2dskia {
13
+
14
+ namespace {
15
+
16
+ using namespace Live2D::Cubism::Framework;
17
+
18
+ class Allocator : public ICubismAllocator {
19
+ public:
20
+ void* Allocate(const csmSizeType size) override { return std::malloc(size); }
21
+ void Deallocate(void* memory) override { std::free(memory); }
22
+ void* AllocateAligned(const csmSizeType size, const csmUint32 alignment) override {
23
+ void* p = nullptr;
24
+ // posix_memalign needs at least pointer alignment.
25
+ const size_t a = alignment < sizeof(void*) ? sizeof(void*) : alignment;
26
+ return posix_memalign(&p, a, size) == 0 ? p : nullptr;
27
+ }
28
+ void DeallocateAligned(void* alignedMemory) override { std::free(alignedMemory); }
29
+ };
30
+
31
+ void log(const char* message) {
32
+ #ifdef __ANDROID__
33
+ __android_log_print(ANDROID_LOG_INFO, "Live2dSkia", "%s", message);
34
+ #else
35
+ std::printf("[Live2dSkia] %s", message);
36
+ #endif
37
+ }
38
+
39
+ } // namespace
40
+
41
+ void ensureCubismFramework() {
42
+ static std::once_flag once;
43
+ std::call_once(once, [] {
44
+ // Both live for the whole process: the Framework keeps pointers to them.
45
+ static Allocator allocator;
46
+ static CubismFramework::Option option{};
47
+ option.LogFunction = log;
48
+ option.LoggingLevel = CubismFramework::Option::LogLevel_Info;
49
+ CubismFramework::StartUp(&allocator, &option);
50
+ CubismFramework::Initialize();
51
+ });
52
+ }
53
+
54
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,9 @@
1
+ #pragma once
2
+
3
+ namespace margelo::nitro::live2dskia {
4
+
5
+ // Starts up and initializes the Cubism Framework once per process.
6
+ // Safe to call from any thread, any number of times.
7
+ void ensureCubismFramework();
8
+
9
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,25 @@
1
+ #pragma once
2
+ #include <map>
3
+ #include "CubismModelSettingJson.hpp"
4
+ #include "Motion/ICubismUpdater.hpp"
5
+
6
+ namespace margelo::nitro::live2dskia {
7
+
8
+ // Applies setParameter values every update, after look and breath (so they
9
+ // can be overridden) and before physics (so hair and clothes follow).
10
+ class HeldParameterUpdater : public Live2D::Cubism::Framework::ICubismUpdater {
11
+ public:
12
+ explicit HeldParameterUpdater(const std::map<Live2D::Cubism::Framework::CubismIdHandle, float>& values)
13
+ : ICubismUpdater(Live2D::Cubism::Framework::CubismUpdateOrder_Breath + 50), _values(values) {}
14
+
15
+ void OnLateUpdate(Live2D::Cubism::Framework::CubismModel* model, Live2D::Cubism::Framework::csmFloat32) override {
16
+ for (const auto& [id, value] : _values) {
17
+ model->SetParameterValue(id, value);
18
+ }
19
+ }
20
+
21
+ private:
22
+ const std::map<Live2D::Cubism::Framework::CubismIdHandle, float>& _values;
23
+ };
24
+
25
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,125 @@
1
+ #include "Live2DUserModel.hpp"
2
+ #include <stdexcept>
3
+ #include "CubismDefaultParameterId.hpp"
4
+ #include "CubismHelpers.hpp"
5
+ #include "Effect/CubismBreath.hpp"
6
+ #include "Effect/CubismEyeBlink.hpp"
7
+ #include "Effect/CubismLook.hpp"
8
+ #include "HeldParameterUpdater.hpp"
9
+ #include "Motion/CubismBreathUpdater.hpp"
10
+ #include "Motion/CubismExpressionUpdater.hpp"
11
+ #include "Motion/CubismEyeBlinkUpdater.hpp"
12
+ #include "Motion/CubismLookUpdater.hpp"
13
+
14
+ namespace margelo::nitro::live2dskia {
15
+
16
+ using namespace Live2D::Cubism::Framework;
17
+
18
+ Live2DUserModel::Live2DUserModel(const uint8_t* moc, size_t mocSize, const std::string& settingsJson) {
19
+ if (moc == nullptr || mocSize == 0) {
20
+ throw std::runtime_error("Live2D: moc is empty");
21
+ }
22
+ LoadModel(moc, static_cast<csmSizeInt>(mocSize), true);
23
+ if (_model == nullptr) {
24
+ throw std::runtime_error("Live2D: moc is invalid, corrupted or newer than this Cubism Core");
25
+ }
26
+
27
+ requireValidJson(settingsJson, "model3.json");
28
+ _setting = CSM_NEW CubismModelSettingJson(bytesOf(settingsJson), static_cast<csmSizeInt>(settingsJson.size()));
29
+
30
+ for (csmInt32 i = 0; i < _setting->GetEyeBlinkParameterCount(); ++i) {
31
+ _eyeBlinkIds.PushBack(_setting->GetEyeBlinkParameterId(i));
32
+ }
33
+ for (csmInt32 i = 0; i < _setting->GetLipSyncParameterCount(); ++i) {
34
+ _lipSyncIds.PushBack(_setting->GetLipSyncParameterId(i));
35
+ }
36
+
37
+ if (_eyeBlinkIds.GetSize() > 0) {
38
+ _eyeBlink = CubismEyeBlink::Create(_setting);
39
+ _updateScheduler.AddUpdatableList(CSM_NEW CubismEyeBlinkUpdater(_motionUpdated, *_eyeBlink));
40
+ }
41
+
42
+ _breath = CubismBreath::Create();
43
+ csmVector<CubismBreath::BreathParameterData> breath;
44
+ breath.PushBack(CubismBreath::BreathParameterData(idOf(DefaultParameterId::ParamAngleX), 0.0f, 15.0f, 6.5345f, 0.5f));
45
+ breath.PushBack(CubismBreath::BreathParameterData(idOf(DefaultParameterId::ParamAngleY), 0.0f, 8.0f, 3.5345f, 0.5f));
46
+ breath.PushBack(CubismBreath::BreathParameterData(idOf(DefaultParameterId::ParamAngleZ), 0.0f, 10.0f, 5.5345f, 0.5f));
47
+ breath.PushBack(CubismBreath::BreathParameterData(idOf(DefaultParameterId::ParamBodyAngleX), 0.0f, 4.0f, 15.5345f, 0.5f));
48
+ breath.PushBack(CubismBreath::BreathParameterData(idOf(DefaultParameterId::ParamBreath), 0.5f, 0.5f, 3.2345f, 0.5f));
49
+ _breath->SetParameters(breath);
50
+ _updateScheduler.AddUpdatableList(CSM_NEW CubismBreathUpdater(*_breath));
51
+
52
+ _updateScheduler.AddUpdatableList(CSM_NEW CubismExpressionUpdater(*_expressionManager));
53
+
54
+ _look = CubismLook::Create();
55
+ csmVector<CubismLook::LookParameterData> look;
56
+ look.PushBack(CubismLook::LookParameterData(idOf(DefaultParameterId::ParamAngleX), 30.0f));
57
+ look.PushBack(CubismLook::LookParameterData(idOf(DefaultParameterId::ParamAngleY), 0.0f, 30.0f));
58
+ look.PushBack(CubismLook::LookParameterData(idOf(DefaultParameterId::ParamAngleZ), 0.0f, 0.0f, -30.0f));
59
+ look.PushBack(CubismLook::LookParameterData(idOf(DefaultParameterId::ParamBodyAngleX), 10.0f));
60
+ look.PushBack(CubismLook::LookParameterData(idOf(DefaultParameterId::ParamEyeBallX), 1.0f));
61
+ look.PushBack(CubismLook::LookParameterData(idOf(DefaultParameterId::ParamEyeBallY), 0.0f, 1.0f));
62
+ _look->SetParameters(look);
63
+ _updateScheduler.AddUpdatableList(CSM_NEW CubismLookUpdater(*_look, *_dragManager));
64
+
65
+ _updateScheduler.AddUpdatableList(CSM_NEW HeldParameterUpdater(_heldParameters));
66
+
67
+ _updateScheduler.SortUpdatableList();
68
+ _model->SaveParameters();
69
+ }
70
+
71
+ Live2DUserModel::~Live2DUserModel() {
72
+ // Stop motions before deleting them: the motion manager still points at them.
73
+ if (_motionManager) _motionManager->StopAllMotions();
74
+ for (auto& [_, motion] : _motions) {
75
+ ACubismMotion::Delete(motion);
76
+ }
77
+ if (_expressionManager) _expressionManager->StopAllMotions();
78
+ for (auto& [_, expression] : _expressions) {
79
+ ACubismMotion::Delete(expression);
80
+ }
81
+ CSM_DELETE(_setting);
82
+ }
83
+
84
+ void Live2DUserModel::setParameter(const std::string& id, float value) {
85
+ _heldParameters[idOf(id.c_str())] = value;
86
+ }
87
+
88
+ void Live2DUserModel::clearParameter(const std::string& id) {
89
+ _heldParameters.erase(idOf(id.c_str()));
90
+ }
91
+
92
+ void Live2DUserModel::focus(float x, float y) {
93
+ SetDragging(x, y);
94
+ }
95
+
96
+ std::vector<std::string> Live2DUserModel::hitTest(float x, float y) {
97
+ std::vector<std::string> hits;
98
+ // Like the SDK sample: a fading-in or transparent model can't be hit.
99
+ if (_model->GetModelOpacity() < 1) return hits;
100
+ for (csmInt32 i = 0; i < _setting->GetHitAreasCount(); ++i) {
101
+ if (IsHit(_setting->GetHitAreaId(i), x, y)) {
102
+ hits.emplace_back(_setting->GetHitAreaName(i));
103
+ }
104
+ }
105
+ return hits;
106
+ }
107
+
108
+ void Live2DUserModel::update(float deltaSeconds) {
109
+ // Same order as the SDK sample's LAppModel::Update.
110
+ _motionUpdated = false;
111
+ _model->LoadParameters();
112
+ if (_motionManager->IsFinished()) {
113
+ startRandomMotion("Idle", PriorityIdle);
114
+ } else {
115
+ _motionUpdated = _motionManager->UpdateMotion(_model, deltaSeconds);
116
+ }
117
+ _model->SaveParameters();
118
+
119
+ // Eye blink (unless a motion moved the eyes), expression, look, breath,
120
+ // held parameters, physics, pose.
121
+ _updateScheduler.OnLateUpdate(_model, deltaSeconds);
122
+ _model->Update();
123
+ }
124
+
125
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,57 @@
1
+ #pragma once
2
+ #include <map>
3
+ #include <string>
4
+ #include <vector>
5
+ #include "CubismModelSettingJson.hpp"
6
+ #include "Model/CubismUserModel.hpp"
7
+
8
+ namespace margelo::nitro::live2dskia {
9
+
10
+ namespace csm = Live2D::Cubism::Framework;
11
+
12
+ // Drives a model with the Cubism Framework, like the SDK sample's LAppModel,
13
+ // without its renderer: drawing is done with Skia from the drawable data.
14
+ // Never call CreateRenderer() on it (see CubismRendererStub.cpp).
15
+ // Not thread-safe: HybridLive2DModel serializes calls.
16
+ class Live2DUserModel : public csm::CubismUserModel {
17
+ public:
18
+ enum Priority { PriorityIdle = 1, PriorityNormal = 2, PriorityForce = 3 };
19
+
20
+ // Throws std::runtime_error if the moc or the settings can't be loaded.
21
+ Live2DUserModel(const uint8_t* moc, size_t mocSize, const std::string& settingsJson);
22
+ ~Live2DUserModel() override;
23
+
24
+ csm::CubismModel* model() const { return _model; }
25
+
26
+ void loadPhysics(const std::string& json);
27
+ void loadPose(const std::string& json);
28
+ void loadMotion(const std::string& group, int index, const std::string& json);
29
+ bool startMotion(const std::string& group, int index, Priority priority);
30
+ bool startRandomMotion(const std::string& group, Priority priority);
31
+ void loadExpression(const std::string& name, const std::string& json);
32
+ bool setExpression(const std::string& name);
33
+ bool setRandomExpression();
34
+ void clearExpression();
35
+ void setParameter(const std::string& id, float value);
36
+ void clearParameter(const std::string& id);
37
+ // -1..1 on each axis, y-up.
38
+ void focus(float x, float y);
39
+ // Hit area names containing the point (model units, y-up).
40
+ std::vector<std::string> hitTest(float x, float y);
41
+ void update(float deltaSeconds);
42
+
43
+ private:
44
+ static std::string motionKey(const std::string& group, int index);
45
+
46
+ csm::CubismModelSettingJson* _setting = nullptr;
47
+ csm::csmVector<csm::CubismIdHandle> _eyeBlinkIds;
48
+ csm::csmVector<csm::CubismIdHandle> _lipSyncIds;
49
+ std::map<std::string, csm::ACubismMotion*> _motions;
50
+ std::map<std::string, csm::ACubismMotion*> _expressions;
51
+ // Set by setParameter, applied every update.
52
+ std::map<csm::CubismIdHandle, float> _heldParameters;
53
+ // Read by the eye blink updater: no blinking while a motion moves the eyes.
54
+ csm::csmBool _motionUpdated = false;
55
+ };
56
+
57
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,107 @@
1
+ // The loadable animation data of Live2DUserModel: physics, pose, motions and expressions.
2
+ #include "Live2DUserModel.hpp"
3
+ #include <cstdlib>
4
+ #include <iterator>
5
+ #include <stdexcept>
6
+ #include "CubismHelpers.hpp"
7
+ #include "Motion/CubismMotion.hpp"
8
+ #include "Motion/CubismPhysicsUpdater.hpp"
9
+ #include "Motion/CubismPoseUpdater.hpp"
10
+
11
+ namespace margelo::nitro::live2dskia {
12
+
13
+ using namespace Live2D::Cubism::Framework;
14
+
15
+ void Live2DUserModel::loadPhysics(const std::string& json) {
16
+ if (_physics) throw std::runtime_error("Live2D: physics already loaded");
17
+ requireValidJson(json, "physics3.json");
18
+ LoadPhysics(bytesOf(json), static_cast<csmSizeInt>(json.size()));
19
+ if (!_physics) throw std::runtime_error("Live2D: invalid physics3.json");
20
+ _updateScheduler.AddUpdatableList(CSM_NEW CubismPhysicsUpdater(*_physics));
21
+ _updateScheduler.SortUpdatableList();
22
+ }
23
+
24
+ void Live2DUserModel::loadPose(const std::string& json) {
25
+ if (_pose) throw std::runtime_error("Live2D: pose already loaded");
26
+ requireValidJson(json, "pose3.json");
27
+ LoadPose(bytesOf(json), static_cast<csmSizeInt>(json.size()));
28
+ if (!_pose) throw std::runtime_error("Live2D: invalid pose3.json");
29
+ _updateScheduler.AddUpdatableList(CSM_NEW CubismPoseUpdater(*_pose));
30
+ _updateScheduler.SortUpdatableList();
31
+ }
32
+
33
+ void Live2DUserModel::loadMotion(const std::string& group, int index, const std::string& json) {
34
+ const std::string key = motionKey(group, index);
35
+ requireValidJson(json, "motion3.json for " + key);
36
+ auto* motion = static_cast<CubismMotion*>(LoadMotion(
37
+ bytesOf(json), static_cast<csmSizeInt>(json.size()), key.c_str(),
38
+ nullptr, nullptr, _setting, group.c_str(), index));
39
+ if (!motion) throw std::runtime_error("Live2D: invalid motion3.json for " + key);
40
+ motion->SetEffectIds(_eyeBlinkIds, _lipSyncIds);
41
+
42
+ auto it = _motions.find(key);
43
+ if (it != _motions.end()) {
44
+ _motionManager->StopAllMotions();
45
+ ACubismMotion::Delete(it->second);
46
+ }
47
+ _motions[key] = motion;
48
+ }
49
+
50
+ bool Live2DUserModel::startMotion(const std::string& group, int index, Priority priority) {
51
+ auto it = _motions.find(motionKey(group, index));
52
+ if (it == _motions.end()) return false;
53
+
54
+ if (priority == PriorityForce) {
55
+ _motionManager->SetReservePriority(priority);
56
+ } else if (!_motionManager->ReserveMotion(priority)) {
57
+ return false;
58
+ }
59
+ // autoDelete = false: the motion is reused, and freed in the destructor.
60
+ _motionManager->StartMotionPriority(it->second, false, priority);
61
+ return true;
62
+ }
63
+
64
+ bool Live2DUserModel::startRandomMotion(const std::string& group, Priority priority) {
65
+ int count = 0;
66
+ while (_motions.count(motionKey(group, count))) ++count;
67
+ if (count == 0) return false;
68
+ return startMotion(group, std::rand() % count, priority);
69
+ }
70
+
71
+ void Live2DUserModel::loadExpression(const std::string& name, const std::string& json) {
72
+ requireValidJson(json, "exp3.json for " + name);
73
+ ACubismMotion* expression = LoadExpression(bytesOf(json), static_cast<csmSizeInt>(json.size()), name.c_str());
74
+ if (!expression) throw std::runtime_error("Live2D: invalid exp3.json for " + name);
75
+
76
+ auto it = _expressions.find(name);
77
+ if (it != _expressions.end()) {
78
+ _expressionManager->StopAllMotions();
79
+ ACubismMotion::Delete(it->second);
80
+ }
81
+ _expressions[name] = expression;
82
+ }
83
+
84
+ bool Live2DUserModel::setExpression(const std::string& name) {
85
+ auto it = _expressions.find(name);
86
+ if (it == _expressions.end()) return false;
87
+ // autoDelete = false: the expression is reused, and freed in the destructor.
88
+ _expressionManager->StartMotion(it->second, false);
89
+ return true;
90
+ }
91
+
92
+ bool Live2DUserModel::setRandomExpression() {
93
+ if (_expressions.empty()) return false;
94
+ auto it = _expressions.begin();
95
+ std::advance(it, std::rand() % _expressions.size());
96
+ return setExpression(it->first);
97
+ }
98
+
99
+ void Live2DUserModel::clearExpression() {
100
+ _expressionManager->StopAllMotions();
101
+ }
102
+
103
+ std::string Live2DUserModel::motionKey(const std::string& group, int index) {
104
+ return group + "_" + std::to_string(index);
105
+ }
106
+
107
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,25 @@
1
+ #pragma once
2
+ #if LIVE2D_SKIA
3
+
4
+ #include <jsi/jsi.h>
5
+ #include <memory>
6
+ #include <string>
7
+
8
+ namespace margelo::nitro::live2dskia {
9
+
10
+ namespace jsi = facebook::jsi;
11
+
12
+ // The wrapper behind a react-native-skia JS object. jsi's hasNativeState<T>
13
+ // uses dynamic_cast, which can't be relied on across shared libraries, so check
14
+ // for any native state and cast: only react-native-live2d-skia's own JS passes these in.
15
+ template <typename T>
16
+ std::shared_ptr<T> unwrap(jsi::Runtime& runtime, const jsi::Value& value, const char* what) {
17
+ if (!value.isObject()) throw jsi::JSError(runtime, std::string("Live2D: expected ") + what);
18
+ jsi::Object object = value.getObject(runtime);
19
+ if (!object.hasNativeState(runtime)) throw jsi::JSError(runtime, std::string("Live2D: expected ") + what);
20
+ return std::static_pointer_cast<T>(object.getNativeState(runtime));
21
+ }
22
+
23
+ } // namespace margelo::nitro::live2dskia
24
+
25
+ #endif // LIVE2D_SKIA