@airisu-dev/react-native-live2d-skia 0.0.0-stage → 0.1.1
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.
- package/LICENSE +21 -0
- package/Live2dSkia.podspec +159 -0
- package/README.md +258 -2
- package/android/CMakeLists.txt +113 -0
- package/android/build.gradle +223 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/live2dskia/Live2dSkiaPackage.kt +20 -0
- package/app.plugin.js +2 -0
- package/cli/build/index.js +157 -0
- package/cpp/cubism/CubismCompat.hpp +26 -0
- package/cpp/cubism/CubismHelpers.hpp +27 -0
- package/cpp/cubism/CubismRendererStub.cpp +14 -0
- package/cpp/cubism/CubismSetup.cpp +54 -0
- package/cpp/cubism/CubismSetup.hpp +9 -0
- package/cpp/cubism/HeldParameterUpdater.hpp +25 -0
- package/cpp/cubism/Live2DUserModel.cpp +125 -0
- package/cpp/cubism/Live2DUserModel.hpp +57 -0
- package/cpp/cubism/Live2DUserModelAnimation.cpp +107 -0
- package/cpp/drawing/JsiUnwrap.hpp +25 -0
- package/cpp/drawing/MeshCache.cpp +75 -0
- package/cpp/drawing/MeshCache.hpp +45 -0
- package/cpp/drawing/SkiaDrawer.cpp +138 -0
- package/cpp/drawing/SkiaDrawer.hpp +42 -0
- package/cpp/hybrid/HybridLive2DModel.cpp +174 -0
- package/cpp/hybrid/HybridLive2DModel.hpp +64 -0
- package/cpp/hybrid/HybridLive2dSkia.cpp +10 -0
- package/cpp/hybrid/HybridLive2dSkia.hpp +11 -0
- package/ios/Bridge.h +8 -0
- package/nitro.json +20 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/Live2dSkia+autolinking.cmake +82 -0
- package/nitrogen/generated/android/Live2dSkia+autolinking.gradle +27 -0
- package/nitrogen/generated/android/Live2dSkiaOnLoad.cpp +49 -0
- package/nitrogen/generated/android/Live2dSkiaOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/live2dskia/Live2dSkiaOnLoad.kt +35 -0
- package/nitrogen/generated/ios/Live2dSkia+autolinking.rb +62 -0
- package/nitrogen/generated/ios/Live2dSkia-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/Live2dSkia-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/Live2dSkia-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/Live2dSkiaAutolinking.mm +35 -0
- package/nitrogen/generated/ios/Live2dSkiaAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/DrawableData.hpp +116 -0
- package/nitrogen/generated/shared/c++/HybridLive2DModelSpec.cpp +39 -0
- package/nitrogen/generated/shared/c++/HybridLive2DModelSpec.hpp +86 -0
- package/nitrogen/generated/shared/c++/HybridLive2dSkiaSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridLive2dSkiaSpec.hpp +66 -0
- package/nitrogen/generated/shared/c++/MotionPriority.hpp +80 -0
- package/package.json +149 -4
- package/plugin/build/index.d.ts +14 -0
- package/plugin/build/index.js +81 -0
- package/src/components/Live2D.tsx +131 -0
- package/src/components/index.ts +2 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useLive2DGesture.ts +140 -0
- package/src/hooks/useLive2DModel.ts +47 -0
- package/src/index.ts +5 -0
- package/src/loader/cubismJson.ts +23 -0
- package/src/loader/index.ts +9 -0
- package/src/loader/loadModel.ts +195 -0
- package/src/loader/modelContext.ts +80 -0
- package/src/loader/modelUrl.ts +112 -0
- package/src/native.ts +4 -0
- package/src/rendering/decodeImage.ts +12 -0
- package/src/rendering/drawer.ts +256 -0
- package/src/rendering/skia.ts +26 -0
- package/src/rendering/skiaTypes.ts +99 -0
- package/src/rendering/ticker/addToTicker.ts +52 -0
- package/src/rendering/ticker/constants.ts +11 -0
- package/src/rendering/ticker/frames.ts +35 -0
- package/src/rendering/ticker/index.ts +5 -0
- package/src/rendering/ticker/loop.ts +115 -0
- package/src/rendering/ticker/state.ts +65 -0
- package/src/rendering/ticker/stats.ts +36 -0
- package/src/rendering/ticker/types.ts +57 -0
- package/src/specs/react-native-live2d-skia.nitro.ts +88 -0
- package/src/utils/clock.ts +10 -0
|
@@ -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
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#if LIVE2D_SKIA
|
|
2
|
+
|
|
3
|
+
#include "MeshCache.hpp"
|
|
4
|
+
#include <chrono>
|
|
5
|
+
#include <cstring>
|
|
6
|
+
|
|
7
|
+
namespace margelo::nitro::live2dskia {
|
|
8
|
+
|
|
9
|
+
using namespace Live2D::Cubism::Core;
|
|
10
|
+
|
|
11
|
+
namespace {
|
|
12
|
+
|
|
13
|
+
double millisecondsSince(std::chrono::steady_clock::time_point start) {
|
|
14
|
+
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
} // namespace
|
|
18
|
+
|
|
19
|
+
void MeshCache::setTextureSizes(std::vector<SkISize> sizes) {
|
|
20
|
+
_textureSizes = std::move(sizes);
|
|
21
|
+
_meshes.clear();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
void MeshCache::resize(size_t drawableCount) {
|
|
25
|
+
if (_meshes.size() != drawableCount) _meshes.resize(drawableCount);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
void MeshCache::resetStats() {
|
|
29
|
+
_meshesBuilt = 0;
|
|
30
|
+
_meshMs = 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SkVertices* MeshCache::meshOf(const csmModel* model, int i, const CanvasInfo& info) {
|
|
34
|
+
const int vertexCount = csmGetDrawableVertexCounts(model)[i];
|
|
35
|
+
const int indexCount = csmGetDrawableIndexCounts(model)[i];
|
|
36
|
+
const int texture = csmGetDrawableTextureIndices(model)[i];
|
|
37
|
+
if (vertexCount == 0 || indexCount == 0 || texture < 0 ||
|
|
38
|
+
texture >= static_cast<int>(_textureSizes.size())) {
|
|
39
|
+
return nullptr;
|
|
40
|
+
}
|
|
41
|
+
Mesh& mesh = _meshes[i];
|
|
42
|
+
const csmVector2* positions = csmGetDrawableVertexPositions(model)[i];
|
|
43
|
+
if (mesh.vertices && mesh.positions.size() == static_cast<size_t>(vertexCount) &&
|
|
44
|
+
std::memcmp(mesh.positions.data(), positions, vertexCount * sizeof(csmVector2)) == 0) {
|
|
45
|
+
return mesh.vertices.get();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const auto start = std::chrono::steady_clock::now();
|
|
49
|
+
if (mesh.texCoords.size() != static_cast<size_t>(vertexCount)) {
|
|
50
|
+
const csmVector2* uvs = csmGetDrawableVertexUvs(model)[i];
|
|
51
|
+
const SkISize size = _textureSizes[texture];
|
|
52
|
+
mesh.texCoords.resize(vertexCount);
|
|
53
|
+
for (int v = 0; v < vertexCount; ++v) {
|
|
54
|
+
// Cubism UVs are v-up.
|
|
55
|
+
mesh.texCoords[v] = {uvs[v].X * size.width(), (1 - uvs[v].Y) * size.height()};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
_points.resize(vertexCount);
|
|
59
|
+
for (int v = 0; v < vertexCount; ++v) {
|
|
60
|
+
// Cubism is y-up in model units; Skia is y-down in pixels.
|
|
61
|
+
_points[v] = {info.originX + positions[v].X * info.pixelsPerUnit,
|
|
62
|
+
info.originY - positions[v].Y * info.pixelsPerUnit};
|
|
63
|
+
}
|
|
64
|
+
mesh.vertices = SkVertices::MakeCopy(SkVertices::kTriangles_VertexMode, vertexCount, _points.data(),
|
|
65
|
+
mesh.texCoords.data(), nullptr, indexCount,
|
|
66
|
+
csmGetDrawableIndices(model)[i]);
|
|
67
|
+
mesh.positions.assign(positions, positions + vertexCount);
|
|
68
|
+
_meshesBuilt++;
|
|
69
|
+
_meshMs += millisecondsSince(start);
|
|
70
|
+
return mesh.vertices.get();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
} // namespace margelo::nitro::live2dskia
|
|
74
|
+
|
|
75
|
+
#endif // LIVE2D_SKIA
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#if LIVE2D_SKIA
|
|
3
|
+
|
|
4
|
+
#include <vector>
|
|
5
|
+
#include "SkiaDrawer.hpp"
|
|
6
|
+
#include "include/core/SkPoint.h"
|
|
7
|
+
#include "include/core/SkRefCnt.h"
|
|
8
|
+
#include "include/core/SkSize.h"
|
|
9
|
+
#include "include/core/SkVertices.h"
|
|
10
|
+
|
|
11
|
+
namespace margelo::nitro::live2dskia {
|
|
12
|
+
|
|
13
|
+
// One Skia mesh per drawable, rebuilt only when the drawable's vertices moved.
|
|
14
|
+
class MeshCache {
|
|
15
|
+
public:
|
|
16
|
+
// Texture coordinates depend on the texture sizes, so this drops every mesh.
|
|
17
|
+
void setTextureSizes(std::vector<SkISize> sizes);
|
|
18
|
+
void resize(size_t drawableCount);
|
|
19
|
+
void resetStats();
|
|
20
|
+
|
|
21
|
+
// Returns the drawable's mesh, rebuilt if its vertices moved; null if it has none.
|
|
22
|
+
const SkVertices* meshOf(const Live2D::Cubism::Core::csmModel* model, int i, const CanvasInfo& info);
|
|
23
|
+
|
|
24
|
+
int meshesBuilt() const { return _meshesBuilt; }
|
|
25
|
+
double meshMs() const { return _meshMs; }
|
|
26
|
+
|
|
27
|
+
private:
|
|
28
|
+
struct Mesh {
|
|
29
|
+
sk_sp<SkVertices> vertices;
|
|
30
|
+
// Model-space positions the mesh was built from, to detect changes.
|
|
31
|
+
std::vector<Live2D::Cubism::Core::csmVector2> positions;
|
|
32
|
+
// Texture coordinates in pixels, built once.
|
|
33
|
+
std::vector<SkPoint> texCoords;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
std::vector<SkISize> _textureSizes;
|
|
37
|
+
std::vector<Mesh> _meshes;
|
|
38
|
+
std::vector<SkPoint> _points; // scratch
|
|
39
|
+
int _meshesBuilt = 0;
|
|
40
|
+
double _meshMs = 0;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
} // namespace margelo::nitro::live2dskia
|
|
44
|
+
|
|
45
|
+
#endif // LIVE2D_SKIA
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#if LIVE2D_SKIA
|
|
2
|
+
|
|
3
|
+
#include "SkiaDrawer.hpp"
|
|
4
|
+
#include "CubismCompat.hpp"
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <numeric>
|
|
7
|
+
#include "JsiSkCanvas.h"
|
|
8
|
+
#include "JsiSkShader.h"
|
|
9
|
+
#include "JsiUnwrap.hpp"
|
|
10
|
+
#include "MeshCache.hpp"
|
|
11
|
+
#include "include/core/SkCanvas.h"
|
|
12
|
+
#include "include/core/SkPaint.h"
|
|
13
|
+
#include "include/core/SkShader.h"
|
|
14
|
+
#include "include/core/SkVertices.h"
|
|
15
|
+
|
|
16
|
+
namespace margelo::nitro::live2dskia {
|
|
17
|
+
|
|
18
|
+
using namespace Live2D::Cubism::Core;
|
|
19
|
+
|
|
20
|
+
namespace {
|
|
21
|
+
|
|
22
|
+
SkBlendMode blendModeOf(csmFlags constantFlags) {
|
|
23
|
+
if (constantFlags & csmBlendAdditive) return SkBlendMode::kPlus;
|
|
24
|
+
if (constantFlags & csmBlendMultiplicative) return SkBlendMode::kMultiply;
|
|
25
|
+
return SkBlendMode::kSrcOver;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
} // namespace
|
|
29
|
+
|
|
30
|
+
struct SkiaDrawer::Impl {
|
|
31
|
+
std::vector<sk_sp<SkShader>> shaders;
|
|
32
|
+
MeshCache meshes;
|
|
33
|
+
std::vector<int> order; // scratch
|
|
34
|
+
SkPaint paint;
|
|
35
|
+
SkPaint layerPaint;
|
|
36
|
+
|
|
37
|
+
void drawMesh(SkCanvas* canvas, const SkVertices* vertices, int texture, SkPaint& p) {
|
|
38
|
+
p.setShader(shaders[texture]);
|
|
39
|
+
// No per-vertex colors, so the vertex blend mode is unused; the shader alone colors the mesh.
|
|
40
|
+
canvas->drawVertices(vertices, SkBlendMode::kDst, p);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
SkiaDrawer::SkiaDrawer() : _impl(std::make_unique<Impl>()) {
|
|
45
|
+
_impl->paint.setAntiAlias(true);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
SkiaDrawer::~SkiaDrawer() = default;
|
|
49
|
+
|
|
50
|
+
void SkiaDrawer::setTextures(jsi::Runtime& runtime, const jsi::Value& shaders, const jsi::Value& sizes) {
|
|
51
|
+
jsi::Array shaderArray = shaders.asObject(runtime).asArray(runtime);
|
|
52
|
+
jsi::Array sizeArray = sizes.asObject(runtime).asArray(runtime);
|
|
53
|
+
const size_t count = shaderArray.size(runtime);
|
|
54
|
+
if (sizeArray.size(runtime) != count * 2) {
|
|
55
|
+
throw jsi::JSError(runtime, "Live2D: expected a width and height per texture");
|
|
56
|
+
}
|
|
57
|
+
_impl->shaders.clear();
|
|
58
|
+
std::vector<SkISize> textureSizes;
|
|
59
|
+
for (size_t i = 0; i < count; ++i) {
|
|
60
|
+
auto shader = unwrap<RNSkia::JsiSkShader>(runtime, shaderArray.getValueAtIndex(runtime, i), "an SkShader");
|
|
61
|
+
_impl->shaders.push_back(shader->getObject());
|
|
62
|
+
textureSizes.push_back(SkISize::Make(
|
|
63
|
+
static_cast<int>(sizeArray.getValueAtIndex(runtime, i * 2).asNumber()),
|
|
64
|
+
static_cast<int>(sizeArray.getValueAtIndex(runtime, i * 2 + 1).asNumber())));
|
|
65
|
+
}
|
|
66
|
+
_impl->meshes.setTextureSizes(std::move(textureSizes));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
jsi::Value SkiaDrawer::draw(jsi::Runtime& runtime, const jsi::Value& canvasValue, bool wantStats,
|
|
70
|
+
const csmModel* model, const CanvasInfo& info) {
|
|
71
|
+
if (_impl->shaders.empty()) throw jsi::JSError(runtime, "Live2D: call setTextures before draw");
|
|
72
|
+
SkCanvas* canvas = unwrap<RNSkia::JsiSkCanvas>(runtime, canvasValue, "an SkCanvas")->getCanvas();
|
|
73
|
+
Impl& d = *_impl;
|
|
74
|
+
|
|
75
|
+
const int count = csmGetDrawableCount(model);
|
|
76
|
+
d.meshes.resize(count);
|
|
77
|
+
d.meshes.resetStats();
|
|
78
|
+
int layers = 0;
|
|
79
|
+
|
|
80
|
+
const int* renderOrders = cubism::renderOrders(model);
|
|
81
|
+
const csmFlags* dynamicFlags = csmGetDrawableDynamicFlags(model);
|
|
82
|
+
const csmFlags* constantFlags = csmGetDrawableConstantFlags(model);
|
|
83
|
+
const int* textureIndices = csmGetDrawableTextureIndices(model);
|
|
84
|
+
const float* opacities = csmGetDrawableOpacities(model);
|
|
85
|
+
const int* maskCounts = csmGetDrawableMaskCounts(model);
|
|
86
|
+
const int** masks = csmGetDrawableMasks(model);
|
|
87
|
+
|
|
88
|
+
// Draw order: drawable indices sorted by render order.
|
|
89
|
+
std::vector<int>& order = d.order;
|
|
90
|
+
order.resize(count);
|
|
91
|
+
std::iota(order.begin(), order.end(), 0);
|
|
92
|
+
std::sort(order.begin(), order.end(), [&](int a, int b) { return renderOrders[a] < renderOrders[b]; });
|
|
93
|
+
|
|
94
|
+
for (int i : order) {
|
|
95
|
+
if (!(dynamicFlags[i] & csmIsVisible) || opacities[i] <= 0) continue;
|
|
96
|
+
const SkVertices* vertices = d.meshes.meshOf(model, i, info);
|
|
97
|
+
if (!vertices) continue;
|
|
98
|
+
|
|
99
|
+
if (maskCounts[i] == 0) {
|
|
100
|
+
d.paint.setAlphaf(opacities[i]);
|
|
101
|
+
d.paint.setBlendMode(blendModeOf(constantFlags[i]));
|
|
102
|
+
d.drawMesh(canvas, vertices, textureIndices[i], d.paint);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Draw into a layer, cut it with the union of the masks, then composite
|
|
107
|
+
// the layer with the drawable's blend mode.
|
|
108
|
+
const SkRect& bounds = vertices->bounds();
|
|
109
|
+
d.layerPaint.setBlendMode(blendModeOf(constantFlags[i]));
|
|
110
|
+
canvas->saveLayer(&bounds, &d.layerPaint);
|
|
111
|
+
d.paint.setAlphaf(opacities[i]);
|
|
112
|
+
d.paint.setBlendMode(SkBlendMode::kSrcOver);
|
|
113
|
+
d.drawMesh(canvas, vertices, textureIndices[i], d.paint);
|
|
114
|
+
|
|
115
|
+
d.layerPaint.setBlendMode((constantFlags[i] & csmIsInvertedMask) ? SkBlendMode::kDstOut : SkBlendMode::kDstIn);
|
|
116
|
+
canvas->saveLayer(&bounds, &d.layerPaint);
|
|
117
|
+
d.paint.setAlphaf(1);
|
|
118
|
+
for (int m = 0; m < maskCounts[i]; ++m) {
|
|
119
|
+
const int maskIndex = masks[i][m];
|
|
120
|
+
const SkVertices* maskVertices = d.meshes.meshOf(model, maskIndex, info);
|
|
121
|
+
if (maskVertices) d.drawMesh(canvas, maskVertices, textureIndices[maskIndex], d.paint);
|
|
122
|
+
}
|
|
123
|
+
canvas->restore();
|
|
124
|
+
canvas->restore();
|
|
125
|
+
layers += 2;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!wantStats) return jsi::Value::undefined();
|
|
129
|
+
jsi::Object stats(runtime);
|
|
130
|
+
stats.setProperty(runtime, "meshesBuilt", d.meshes.meshesBuilt());
|
|
131
|
+
stats.setProperty(runtime, "layers", layers);
|
|
132
|
+
stats.setProperty(runtime, "meshMs", d.meshes.meshMs());
|
|
133
|
+
return stats;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
} // namespace margelo::nitro::live2dskia
|
|
137
|
+
|
|
138
|
+
#endif // LIVE2D_SKIA
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#if LIVE2D_SKIA
|
|
3
|
+
|
|
4
|
+
#include <jsi/jsi.h>
|
|
5
|
+
#include <memory>
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include "Live2DCubismCore.hpp"
|
|
8
|
+
|
|
9
|
+
namespace margelo::nitro::live2dskia {
|
|
10
|
+
|
|
11
|
+
namespace jsi = facebook::jsi;
|
|
12
|
+
|
|
13
|
+
struct CanvasInfo {
|
|
14
|
+
float originX;
|
|
15
|
+
float originY;
|
|
16
|
+
float pixelsPerUnit;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Draws a model's drawables onto a react-native-skia canvas in C++, the same
|
|
20
|
+
// way src/rendering/drawer.ts does in JS, without building meshes through JSI.
|
|
21
|
+
// Not thread-safe: HybridLive2DModel serializes calls.
|
|
22
|
+
class SkiaDrawer {
|
|
23
|
+
public:
|
|
24
|
+
SkiaDrawer();
|
|
25
|
+
~SkiaDrawer();
|
|
26
|
+
|
|
27
|
+
// shaders: SkShader[] (one per texture); sizes: [width0, height0, width1, ...].
|
|
28
|
+
void setTextures(jsi::Runtime& runtime, const jsi::Value& shaders, const jsi::Value& sizes);
|
|
29
|
+
|
|
30
|
+
// Draws the model's current state onto a JS SkCanvas. Returns
|
|
31
|
+
// { meshesBuilt, layers, meshMs } when wantStats, else undefined.
|
|
32
|
+
jsi::Value draw(jsi::Runtime& runtime, const jsi::Value& canvas, bool wantStats,
|
|
33
|
+
const Live2D::Cubism::Core::csmModel* model, const CanvasInfo& info);
|
|
34
|
+
|
|
35
|
+
private:
|
|
36
|
+
struct Impl;
|
|
37
|
+
std::unique_ptr<Impl> _impl;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
} // namespace margelo::nitro::live2dskia
|
|
41
|
+
|
|
42
|
+
#endif // LIVE2D_SKIA
|