@airisu-dev/react-native-live2d-skia 0.1.2-nightly.20261025.2b7609a → 0.2.0-rc.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/README.md +65 -12
- package/cpp/cubism/HeldParameterUpdater.hpp +27 -7
- package/cpp/cubism/Live2DUserModel.cpp +24 -4
- package/cpp/cubism/Live2DUserModel.hpp +26 -1
- package/cpp/cubism/Live2DUserModelAnimation.cpp +46 -0
- package/cpp/hybrid/HybridLive2DModel.cpp +79 -0
- package/cpp/hybrid/HybridLive2DModel.hpp +13 -0
- package/nitrogen/generated/shared/c++/HybridLive2DModelSpec.cpp +13 -0
- package/nitrogen/generated/shared/c++/HybridLive2DModelSpec.hpp +16 -0
- package/nitrogen/generated/shared/c++/ParameterData.hpp +99 -0
- package/package.json +1 -1
- package/src/components/Live2D.tsx +28 -6
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useLive2DGesture.ts +11 -5
- package/src/hooks/useLive2DModel.ts +1 -1
- package/src/hooks/useLive2DParameters.ts +70 -0
- package/src/loader/index.ts +11 -0
- package/src/loader/loadModel.ts +67 -3
- package/src/loader/modelContext.ts +6 -11
- package/src/loader/modelInfo.ts +68 -0
- package/src/loader/modelTypes.ts +89 -0
- package/src/loader/modelUrl.ts +16 -2
- package/src/rendering/skiaTypes.ts +3 -1
- package/src/specs/react-native-live2d-skia.nitro.ts +44 -0
package/README.md
CHANGED
|
@@ -162,6 +162,17 @@ const gesture = useLive2DGestures([
|
|
|
162
162
|
</GestureDetector>
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
To animate a model, pass Reanimated shared values as `x`, `y` or `scale` (the gesture hooks take the same). It moves on the UI thread, with no React render per frame:
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
const x = useSharedValue(0)
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
x.set(withRepeat(withTiming(80, { duration: 1500 }), -1, true))
|
|
171
|
+
}, [x])
|
|
172
|
+
// …
|
|
173
|
+
<Live2D model={model} x={x} y={0} scale={0.4} />
|
|
174
|
+
```
|
|
175
|
+
|
|
165
176
|
Inside a `<Canvas>`, JSX order is drawing order: a `<Live2D>` is one layer among ordinary Skia shapes. One animation loop updates and draws every shown model each frame, and two `<Live2D>`s showing the same model share its frames.
|
|
166
177
|
|
|
167
178
|
### Controlling the model
|
|
@@ -175,6 +186,10 @@ model.expression('exp_01') // expression; random without a name
|
|
|
175
186
|
model.resetExpression()
|
|
176
187
|
model.setParameter('ParamAngleX', 30) // held over motions until resetParameter
|
|
177
188
|
model.resetParameter('ParamAngleX')
|
|
189
|
+
model.resetParameters() // release every held parameter
|
|
190
|
+
model.getParameter('ParamAngleX') // current value, motions and physics applied
|
|
191
|
+
model.stopMotion() // stops it where it is
|
|
192
|
+
model.resetPose() // stops it and eases back to the default pose
|
|
178
193
|
model.focus(x, y) // look at a point, easing
|
|
179
194
|
model.resetFocus()
|
|
180
195
|
model.hitTest(x, y) // → ['Head']
|
|
@@ -182,6 +197,28 @@ model.hitTest(x, y) // → ['Head']
|
|
|
182
197
|
|
|
183
198
|
Methods can be called from the JS thread while the model animates on its background thread: the native model is locked.
|
|
184
199
|
|
|
200
|
+
### Driving the model from a camera or an animation
|
|
201
|
+
|
|
202
|
+
To drive parameters at a high rate, e.g. from face tracking, keep the JS thread out of it. `useLive2DParameters` holds parameters at the values of a Reanimated shared value, passing them on from the UI thread. Write the shared value from any worklet:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
const values = useSharedValue<Record<string, number>>({})
|
|
206
|
+
useLive2DParameters(model, values, { smoothing: 0.08 })
|
|
207
|
+
// in a worklet: values.value = { ParamAngleX: yaw * 30, ParamEyeLOpen: leftEyeOpen }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
A camera frame processor (e.g. react-native-vision-camera with ML Kit or `react-native-fast-tflite`) can also call the native model directly, in one locked call per frame:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
model.native.setParameters(['ParamAngleX', 'ParamEyeLOpen'], [yaw * 30, leftEyeOpen])
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- Values are clamped to each parameter's range, and ids the model doesn't have are ignored.
|
|
217
|
+
- Trackers deliver 15-30 values a second and the display shows 60 frames: `smoothing` (a time constant in seconds, `model.setParameterSmoothing(0.08)`) eases held parameters towards their values in between.
|
|
218
|
+
- Typical mapping: head yaw, pitch and roll to `ParamAngleX`, `ParamAngleY` and `ParamAngleZ` (about ±30°, mirrored for a front camera), eye openness to `ParamEyeLOpen` and `ParamEyeROpen`, mouth openness to `ParamMouthOpenY`, smile to `ParamMouthForm`.
|
|
219
|
+
- `useLive2DModel(source, { idleMotion: false })` (or `model.setIdleMotionEnabled(false)` later) stops the random `Idle` motions that start whenever nothing plays, so only your values drive the model. Call `model.resetParameters()` when the face is lost, and the model's own motions take over again.
|
|
220
|
+
- `model.resetPose()` eases what motions moved back to the default pose and leaves held parameters alone, so it can be called mid-motion while tracking runs.
|
|
221
|
+
|
|
185
222
|
## API
|
|
186
223
|
|
|
187
224
|
### `useLive2DModel(source, options?)`
|
|
@@ -199,14 +236,16 @@ Methods can be called from the JS thread while the model animates on its backgro
|
|
|
199
236
|
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
200
237
|
| `headers` | Sent with every request for a remote file |
|
|
201
238
|
| `resolveUrl` | `(url, file) => string \| Promise<string>`: the URL to request for each file the `model3.json` lists. `url` is resolved next to it without a query, `file` is the path as listed. Replaces the default, which reuses the `model3.json` URL's query |
|
|
239
|
+
| `idleMotion` | Whether a random `Idle` motion starts whenever nothing plays, from the first frame. Default `true`; `model.setIdleMotionEnabled` changes it later |
|
|
240
|
+
| `eyeBlink` | Whether the eyes blink, for models with eye blink parameters. Default `true`; `model.setEyeBlinkEnabled` changes it later |
|
|
202
241
|
|
|
203
242
|
### `<Live2D />`
|
|
204
243
|
|
|
205
244
|
| Prop | Description |
|
|
206
245
|
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
207
246
|
| `model` | `model` from `useLive2DModel` (renders nothing while `null`) |
|
|
208
|
-
| `x`, `y` | Top-left position on the canvas. Default `0`
|
|
209
|
-
| `scale` | Uniform scale of the model's native canvas size. Default `1`
|
|
247
|
+
| `x`, `y` | Top-left position on the canvas: a number, or a Reanimated shared value. Default `0` |
|
|
248
|
+
| `scale` | Uniform scale of the model's native canvas size: a number, or a shared value. Default `1` |
|
|
210
249
|
| `paused` | Stops animating, keeps the current frame. Default `false` |
|
|
211
250
|
| `maxFps` | Caps the frame rate (e.g. `30` to save battery). Default: the display's refresh rate |
|
|
212
251
|
| `onFrameStats(stats)` | About once a second: `fps`, `targetFps`, per-frame times (`workMs`, `updateMs`, `drawablesMs`, `drawMs`, `meshMs`), `meshesBuilt`, `layers` (mask layers), `models` and `loopWorkMs` |
|
|
@@ -235,18 +274,32 @@ The gesture is built once; targets are read at touch time, so they can change on
|
|
|
235
274
|
|
|
236
275
|
The same for one model: `options` is a target (without `model`) plus `focus`.
|
|
237
276
|
|
|
277
|
+
### `useLive2DParameters(model, values, options?)`
|
|
278
|
+
|
|
279
|
+
Holds parameters at the values of a `SharedValue<Record<string, number>>` as it changes, from the UI thread, and releases them on unmount. `options.smoothing` is the easing time constant in seconds (default `0`). `useLive2DParameter(model, id, value)` is the same for one parameter and a `SharedValue<number>`.
|
|
280
|
+
|
|
238
281
|
### `Live2DModel`
|
|
239
282
|
|
|
240
|
-
| Member | Description
|
|
241
|
-
| ------------------------------------------------ |
|
|
242
|
-
| `motion(group, index?, priority?)` | Plays a motion; `priority` is `'idle'`, `'normal'` or `'force'`. `false` if there is none or a higher priority one is playing
|
|
243
|
-
| `expression(name?)` | Fades to an expression, random without a name. `false` if there is none
|
|
244
|
-
| `resetExpression()` | Removes the current expression
|
|
245
|
-
| `setParameter(id, value)` / `resetParameter(id)` | Holds a parameter over motions until reset
|
|
246
|
-
| `focus(x, y)` / `resetFocus()` | Looks at a point, easing / looks straight ahead
|
|
247
|
-
| `hitTest(x, y)` | Names of the `model3.json` hit areas at a point
|
|
248
|
-
| `
|
|
249
|
-
| `
|
|
283
|
+
| Member | Description |
|
|
284
|
+
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
285
|
+
| `motion(group, index?, priority?)` | Plays a motion; `priority` is `'idle'`, `'normal'` or `'force'`. `false` if there is none or a higher priority one is playing |
|
|
286
|
+
| `expression(name?)` | Fades to an expression, random without a name. `false` if there is none |
|
|
287
|
+
| `resetExpression()` | Removes the current expression |
|
|
288
|
+
| `setParameter(id, value)` / `resetParameter(id)` | Holds a parameter over motions until reset |
|
|
289
|
+
| `focus(x, y)` / `resetFocus()` | Looks at a point, easing / looks straight ahead |
|
|
290
|
+
| `hitTest(x, y)` | Names of the `model3.json` hit areas at a point |
|
|
291
|
+
| `setParameters(values)` | `setParameter` for each entry of a `{ id: value }` object, in one locked call |
|
|
292
|
+
| `setParameterSmoothing(seconds)` | Eases held parameters towards their values; `0` (default) applies them as they are |
|
|
293
|
+
| `setIdleMotionEnabled(enabled)` | Whether a random `Idle` motion starts when nothing plays. Default `true` |
|
|
294
|
+
| `setEyeBlinkEnabled(enabled)` | Whether the eyes blink, for models with eye blink parameters. Default `true` |
|
|
295
|
+
| `resetParameters()` / `getParameter(id)` | Releases every held parameter / reads a parameter's current value |
|
|
296
|
+
| `stopMotion()` / `isMotionPlaying()` | Stops the playing motion, leaving the model where it was (an `Idle` one starts again if enabled) / whether one plays |
|
|
297
|
+
| `resetPose(fadeSeconds?)` | Stops the playing motion and eases every parameter and switched pose part back to its default (0.5 s by default, `0` snaps); held ones stay |
|
|
298
|
+
| `motions` | `Record<group, MotionInfo[]>`: file, duration, loop and fades of each motion, from the model's files |
|
|
299
|
+
| `expressions` | `ExpressionInfo[]`: name, file and the parameters each expression sets |
|
|
300
|
+
| `parameters` | `ParameterInfo[]`: id, min, max, default, and the name and group from the model's `cdi3.json` if it has one. `setParameter` clamps to the range |
|
|
301
|
+
| `motionGroups`, `expressionNames` | The names in `motions` and `expressions` |
|
|
302
|
+
| `width`, `height` | The model's native canvas size |
|
|
250
303
|
|
|
251
304
|
## Contributing
|
|
252
305
|
|
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
#pragma once
|
|
2
|
+
#include <cmath>
|
|
2
3
|
#include <map>
|
|
3
4
|
#include "CubismModelSettingJson.hpp"
|
|
4
5
|
#include "Motion/ICubismUpdater.hpp"
|
|
5
6
|
|
|
6
7
|
namespace margelo::nitro::live2dskia {
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
+
// A parameter held at `target` by setParameter(s). `current` is what the model gets: the
|
|
10
|
+
// target, or eased towards it when smoothing is on.
|
|
11
|
+
struct HeldParameter {
|
|
12
|
+
float target = 0;
|
|
13
|
+
float current = 0;
|
|
14
|
+
// Set until the first update after holding: `current` starts from the model's own value.
|
|
15
|
+
bool fresh = true;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Applies the held parameters every update, after look and breath (so they
|
|
9
19
|
// can be overridden) and before physics (so hair and clothes follow).
|
|
10
20
|
class HeldParameterUpdater : public Live2D::Cubism::Framework::ICubismUpdater {
|
|
11
21
|
public:
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
// `smoothing` is the time constant in seconds of the easing, 0 for none.
|
|
23
|
+
HeldParameterUpdater(std::map<Live2D::Cubism::Framework::CubismIdHandle, HeldParameter>& values, const float& smoothing)
|
|
24
|
+
: ICubismUpdater(Live2D::Cubism::Framework::CubismUpdateOrder_Breath + 50), _values(values), _smoothing(smoothing) {}
|
|
14
25
|
|
|
15
|
-
void OnLateUpdate(Live2D::Cubism::Framework::CubismModel* model, Live2D::Cubism::Framework::csmFloat32) override {
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
void OnLateUpdate(Live2D::Cubism::Framework::CubismModel* model, Live2D::Cubism::Framework::csmFloat32 deltaSeconds) override {
|
|
27
|
+
const float tau = _smoothing;
|
|
28
|
+
for (auto& [id, held] : _values) {
|
|
29
|
+
if (tau <= 0) {
|
|
30
|
+
held.current = held.target;
|
|
31
|
+
} else {
|
|
32
|
+
if (held.fresh) held.current = model->GetParameterValue(id);
|
|
33
|
+
held.current += (held.target - held.current) * (1.0f - std::exp(-deltaSeconds / tau));
|
|
34
|
+
}
|
|
35
|
+
held.fresh = false;
|
|
36
|
+
model->SetParameterValue(id, held.current);
|
|
18
37
|
}
|
|
19
38
|
}
|
|
20
39
|
|
|
21
40
|
private:
|
|
22
|
-
|
|
41
|
+
std::map<Live2D::Cubism::Framework::CubismIdHandle, HeldParameter>& _values;
|
|
42
|
+
const float& _smoothing;
|
|
23
43
|
};
|
|
24
44
|
|
|
25
45
|
} // namespace margelo::nitro::live2dskia
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "Live2DUserModel.hpp"
|
|
2
|
+
#include <algorithm>
|
|
2
3
|
#include <stdexcept>
|
|
3
4
|
#include "CubismDefaultParameterId.hpp"
|
|
4
5
|
#include "CubismHelpers.hpp"
|
|
@@ -36,7 +37,7 @@ Live2DUserModel::Live2DUserModel(const uint8_t* moc, size_t mocSize, const std::
|
|
|
36
37
|
|
|
37
38
|
if (_eyeBlinkIds.GetSize() > 0) {
|
|
38
39
|
_eyeBlink = CubismEyeBlink::Create(_setting);
|
|
39
|
-
_updateScheduler.AddUpdatableList(CSM_NEW CubismEyeBlinkUpdater(
|
|
40
|
+
_updateScheduler.AddUpdatableList(CSM_NEW CubismEyeBlinkUpdater(_skipEyeBlink, *_eyeBlink));
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
_breath = CubismBreath::Create();
|
|
@@ -62,7 +63,7 @@ Live2DUserModel::Live2DUserModel(const uint8_t* moc, size_t mocSize, const std::
|
|
|
62
63
|
_look->SetParameters(look);
|
|
63
64
|
_updateScheduler.AddUpdatableList(CSM_NEW CubismLookUpdater(*_look, *_dragManager));
|
|
64
65
|
|
|
65
|
-
_updateScheduler.AddUpdatableList(CSM_NEW HeldParameterUpdater(_heldParameters));
|
|
66
|
+
_updateScheduler.AddUpdatableList(CSM_NEW HeldParameterUpdater(_heldParameters, _parameterSmoothing));
|
|
66
67
|
|
|
67
68
|
_updateScheduler.SortUpdatableList();
|
|
68
69
|
_model->SaveParameters();
|
|
@@ -82,13 +83,29 @@ Live2DUserModel::~Live2DUserModel() {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
void Live2DUserModel::setParameter(const std::string& id, float value) {
|
|
85
|
-
|
|
86
|
+
const CubismIdHandle handle = idOf(id.c_str());
|
|
87
|
+
const csmInt32 index = _model->GetParameterIndex(handle);
|
|
88
|
+
if (index < 0) return;
|
|
89
|
+
const float clamped = std::min(_model->GetParameterMaximumValue(index), std::max(_model->GetParameterMinimumValue(index), value));
|
|
90
|
+
// Holding again keeps `current`, so a changing target is eased towards, not restarted.
|
|
91
|
+
_heldParameters[handle].target = clamped;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
void Live2DUserModel::setParameters(const std::vector<std::string>& ids, const std::vector<double>& values) {
|
|
95
|
+
const size_t count = std::min(ids.size(), values.size());
|
|
96
|
+
for (size_t i = 0; i < count; ++i) {
|
|
97
|
+
setParameter(ids[i], static_cast<float>(values[i]));
|
|
98
|
+
}
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
void Live2DUserModel::clearParameter(const std::string& id) {
|
|
89
102
|
_heldParameters.erase(idOf(id.c_str()));
|
|
90
103
|
}
|
|
91
104
|
|
|
105
|
+
void Live2DUserModel::clearParameters() {
|
|
106
|
+
_heldParameters.clear();
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
void Live2DUserModel::focus(float x, float y) {
|
|
93
110
|
SetDragging(x, y);
|
|
94
111
|
}
|
|
@@ -109,8 +126,10 @@ void Live2DUserModel::update(float deltaSeconds) {
|
|
|
109
126
|
// Same order as the SDK sample's LAppModel::Update.
|
|
110
127
|
_motionUpdated = false;
|
|
111
128
|
_model->LoadParameters();
|
|
129
|
+
// Before motions, so one started during the fade blends in from the easing pose.
|
|
130
|
+
if (_poseResetRemaining > 0) easeToDefaultPose(deltaSeconds);
|
|
112
131
|
if (_motionManager->IsFinished()) {
|
|
113
|
-
startRandomMotion("Idle", PriorityIdle);
|
|
132
|
+
if (_idleMotionEnabled) startRandomMotion("Idle", PriorityIdle);
|
|
114
133
|
} else {
|
|
115
134
|
_motionUpdated = _motionManager->UpdateMotion(_model, deltaSeconds);
|
|
116
135
|
}
|
|
@@ -118,6 +137,7 @@ void Live2DUserModel::update(float deltaSeconds) {
|
|
|
118
137
|
|
|
119
138
|
// Eye blink (unless a motion moved the eyes), expression, look, breath,
|
|
120
139
|
// held parameters, physics, pose.
|
|
140
|
+
_skipEyeBlink = _motionUpdated || !_eyeBlinkEnabled;
|
|
121
141
|
_updateScheduler.OnLateUpdate(_model, deltaSeconds);
|
|
122
142
|
_model->Update();
|
|
123
143
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include <string>
|
|
4
4
|
#include <vector>
|
|
5
5
|
#include "CubismModelSettingJson.hpp"
|
|
6
|
+
#include "HeldParameterUpdater.hpp"
|
|
6
7
|
#include "Model/CubismUserModel.hpp"
|
|
7
8
|
|
|
8
9
|
namespace margelo::nitro::live2dskia {
|
|
@@ -28,12 +29,28 @@ class Live2DUserModel : public csm::CubismUserModel {
|
|
|
28
29
|
void loadMotion(const std::string& group, int index, const std::string& json);
|
|
29
30
|
bool startMotion(const std::string& group, int index, Priority priority);
|
|
30
31
|
bool startRandomMotion(const std::string& group, Priority priority);
|
|
32
|
+
void stopMotion();
|
|
33
|
+
bool isMotionPlaying();
|
|
34
|
+
// Stops motions and eases every parameter to its default over `fadeSeconds` (0 snaps).
|
|
35
|
+
void resetPose(float fadeSeconds);
|
|
31
36
|
void loadExpression(const std::string& name, const std::string& json);
|
|
32
37
|
bool setExpression(const std::string& name);
|
|
33
38
|
bool setRandomExpression();
|
|
34
39
|
void clearExpression();
|
|
40
|
+
// Clamped to the parameter's range; ids the model doesn't have are ignored.
|
|
35
41
|
void setParameter(const std::string& id, float value);
|
|
42
|
+
void setParameters(const std::vector<std::string>& ids, const std::vector<double>& values);
|
|
36
43
|
void clearParameter(const std::string& id);
|
|
44
|
+
void clearParameters();
|
|
45
|
+
// Time constant in seconds of the easing of held parameters towards their value, 0 for none.
|
|
46
|
+
float parameterSmoothing() const { return _parameterSmoothing; }
|
|
47
|
+
void setParameterSmoothing(float seconds) { _parameterSmoothing = seconds < 0 ? 0 : seconds; }
|
|
48
|
+
// Whether a random Idle motion starts whenever no motion plays.
|
|
49
|
+
bool idleMotionEnabled() const { return _idleMotionEnabled; }
|
|
50
|
+
void setIdleMotionEnabled(bool enabled) { _idleMotionEnabled = enabled; }
|
|
51
|
+
// Whether the eyes blink (models with eye blink parameters only).
|
|
52
|
+
bool eyeBlinkEnabled() const { return _eyeBlinkEnabled; }
|
|
53
|
+
void setEyeBlinkEnabled(bool enabled) { _eyeBlinkEnabled = enabled; }
|
|
37
54
|
// -1..1 on each axis, y-up.
|
|
38
55
|
void focus(float x, float y);
|
|
39
56
|
// Hit area names containing the point (model units, y-up).
|
|
@@ -49,9 +66,17 @@ class Live2DUserModel : public csm::CubismUserModel {
|
|
|
49
66
|
std::map<std::string, csm::ACubismMotion*> _motions;
|
|
50
67
|
std::map<std::string, csm::ACubismMotion*> _expressions;
|
|
51
68
|
// Set by setParameter, applied every update.
|
|
52
|
-
std::map<csm::CubismIdHandle,
|
|
69
|
+
std::map<csm::CubismIdHandle, HeldParameter> _heldParameters;
|
|
70
|
+
float _parameterSmoothing = 0;
|
|
71
|
+
bool _idleMotionEnabled = true;
|
|
72
|
+
bool _eyeBlinkEnabled = true;
|
|
73
|
+
// Seconds left of a resetPose fade, 0 when none runs.
|
|
74
|
+
float _poseResetRemaining = 0;
|
|
75
|
+
void easeToDefaultPose(float deltaSeconds);
|
|
53
76
|
// Read by the eye blink updater: no blinking while a motion moves the eyes.
|
|
54
77
|
csm::csmBool _motionUpdated = false;
|
|
78
|
+
// Read by the eye blink updater: set while a motion moves the eyes or blinking is off.
|
|
79
|
+
csm::csmBool _skipEyeBlink = false;
|
|
55
80
|
};
|
|
56
81
|
|
|
57
82
|
} // namespace margelo::nitro::live2dskia
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include <cstdlib>
|
|
4
4
|
#include <iterator>
|
|
5
5
|
#include <stdexcept>
|
|
6
|
+
#include <vector>
|
|
6
7
|
#include "CubismHelpers.hpp"
|
|
7
8
|
#include "Motion/CubismMotion.hpp"
|
|
8
9
|
#include "Motion/CubismPhysicsUpdater.hpp"
|
|
@@ -68,6 +69,51 @@ bool Live2DUserModel::startRandomMotion(const std::string& group, Priority prior
|
|
|
68
69
|
return startMotion(group, std::rand() % count, priority);
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
void Live2DUserModel::stopMotion() {
|
|
73
|
+
_motionManager->StopAllMotions();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
bool Live2DUserModel::isMotionPlaying() {
|
|
77
|
+
return !_motionManager->IsFinished();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
void Live2DUserModel::resetPose(float fadeSeconds) {
|
|
81
|
+
_motionManager->StopAllMotions();
|
|
82
|
+
// Motions also switch pose parts (Hiyori's raised arm is PartArmB) through per-part values
|
|
83
|
+
// that aren't model parameters. Reset puts back the first part of each group; restoring the
|
|
84
|
+
// opacities it set lets the pose fade to that part instead of popping.
|
|
85
|
+
if (_pose) {
|
|
86
|
+
const csmInt32 partCount = _model->GetPartCount();
|
|
87
|
+
std::vector<csmFloat32> opacities(static_cast<size_t>(partCount));
|
|
88
|
+
for (csmInt32 i = 0; i < partCount; ++i) opacities[i] = _model->GetPartOpacity(i);
|
|
89
|
+
_pose->Reset(_model);
|
|
90
|
+
if (fadeSeconds > 0) {
|
|
91
|
+
for (csmInt32 i = 0; i < partCount; ++i) _model->SetPartOpacity(i, opacities[i]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// update() starts each frame from the saved parameters, so a stopped motion alone
|
|
95
|
+
// leaves the model frozen in its last pose: move those towards the defaults instead.
|
|
96
|
+
if (fadeSeconds > 0) {
|
|
97
|
+
_poseResetRemaining = fadeSeconds;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
_poseResetRemaining = 0;
|
|
101
|
+
for (csmInt32 i = 0; i < _model->GetParameterCount(); ++i) {
|
|
102
|
+
_model->SetParameterValue(i, _model->GetParameterDefaultValue(i));
|
|
103
|
+
}
|
|
104
|
+
_model->SaveParameters();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
void Live2DUserModel::easeToDefaultPose(float deltaSeconds) {
|
|
108
|
+
// Linear in time: each frame covers its share of what is left, so it lands at the end.
|
|
109
|
+
const float t = deltaSeconds >= _poseResetRemaining ? 1.0f : deltaSeconds / _poseResetRemaining;
|
|
110
|
+
for (csmInt32 i = 0; i < _model->GetParameterCount(); ++i) {
|
|
111
|
+
const float value = _model->GetParameterValue(i);
|
|
112
|
+
_model->SetParameterValue(i, value + (_model->GetParameterDefaultValue(i) - value) * t);
|
|
113
|
+
}
|
|
114
|
+
_poseResetRemaining = t >= 1.0f ? 0 : _poseResetRemaining - deltaSeconds;
|
|
115
|
+
}
|
|
116
|
+
|
|
71
117
|
void Live2DUserModel::loadExpression(const std::string& name, const std::string& json) {
|
|
72
118
|
requireValidJson(json, "exp3.json for " + name);
|
|
73
119
|
ACubismMotion* expression = LoadExpression(bytesOf(json), static_cast<csmSizeInt>(json.size()), name.c_str());
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include <numeric>
|
|
4
4
|
#include "CubismSetup.hpp"
|
|
5
5
|
#include "CubismCompat.hpp"
|
|
6
|
+
#include "CubismHelpers.hpp"
|
|
6
7
|
|
|
7
8
|
namespace margelo::nitro::live2dskia {
|
|
8
9
|
|
|
@@ -106,6 +107,21 @@ bool HybridLive2DModel::startMotion(const std::string& group, std::optional<doub
|
|
|
106
107
|
return _userModel->startMotion(group, static_cast<int>(*index), toPriority(priority));
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
void HybridLive2DModel::stopMotion() {
|
|
111
|
+
std::lock_guard lock(_mutex);
|
|
112
|
+
_userModel->stopMotion();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
bool HybridLive2DModel::isMotionPlaying() {
|
|
116
|
+
std::lock_guard lock(_mutex);
|
|
117
|
+
return _userModel->isMotionPlaying();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
void HybridLive2DModel::resetPose(std::optional<double> fadeSeconds) {
|
|
121
|
+
std::lock_guard lock(_mutex);
|
|
122
|
+
_userModel->resetPose(static_cast<float>(fadeSeconds.value_or(0.5)));
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
void HybridLive2DModel::loadExpression(const std::string& name, const std::string& json) {
|
|
110
126
|
std::lock_guard lock(_mutex);
|
|
111
127
|
_userModel->loadExpression(name, json);
|
|
@@ -131,6 +147,69 @@ void HybridLive2DModel::clearParameter(const std::string& id) {
|
|
|
131
147
|
_userModel->clearParameter(id);
|
|
132
148
|
}
|
|
133
149
|
|
|
150
|
+
std::vector<ParameterData> HybridLive2DModel::getParameters() {
|
|
151
|
+
std::lock_guard lock(_mutex);
|
|
152
|
+
auto* model = _userModel->model();
|
|
153
|
+
const int count = model->GetParameterCount();
|
|
154
|
+
std::vector<ParameterData> result;
|
|
155
|
+
result.reserve(count);
|
|
156
|
+
for (int i = 0; i < count; ++i) {
|
|
157
|
+
result.emplace_back(std::string(model->GetParameterId(i)->GetString().GetRawString()),
|
|
158
|
+
model->GetParameterMinimumValue(i),
|
|
159
|
+
model->GetParameterMaximumValue(i),
|
|
160
|
+
model->GetParameterDefaultValue(i),
|
|
161
|
+
model->GetParameterValue(i));
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
double HybridLive2DModel::getParameterValue(const std::string& id) {
|
|
167
|
+
std::lock_guard lock(_mutex);
|
|
168
|
+
auto* model = _userModel->model();
|
|
169
|
+
const int index = model->GetParameterIndex(idOf(id.c_str()));
|
|
170
|
+
return index < 0 ? 0.0 : model->GetParameterValue(index);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
void HybridLive2DModel::setParameters(const std::vector<std::string>& ids, const std::vector<double>& values) {
|
|
174
|
+
std::lock_guard lock(_mutex);
|
|
175
|
+
_userModel->setParameters(ids, values);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
void HybridLive2DModel::clearParameters() {
|
|
179
|
+
std::lock_guard lock(_mutex);
|
|
180
|
+
_userModel->clearParameters();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
double HybridLive2DModel::getParameterSmoothing() {
|
|
184
|
+
std::lock_guard lock(_mutex);
|
|
185
|
+
return _userModel->parameterSmoothing();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
void HybridLive2DModel::setParameterSmoothing(double seconds) {
|
|
189
|
+
std::lock_guard lock(_mutex);
|
|
190
|
+
_userModel->setParameterSmoothing(static_cast<float>(seconds));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
bool HybridLive2DModel::getIdleMotionEnabled() {
|
|
194
|
+
std::lock_guard lock(_mutex);
|
|
195
|
+
return _userModel->idleMotionEnabled();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
void HybridLive2DModel::setIdleMotionEnabled(bool enabled) {
|
|
199
|
+
std::lock_guard lock(_mutex);
|
|
200
|
+
_userModel->setIdleMotionEnabled(enabled);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
bool HybridLive2DModel::getEyeBlinkEnabled() {
|
|
204
|
+
std::lock_guard lock(_mutex);
|
|
205
|
+
return _userModel->eyeBlinkEnabled();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
void HybridLive2DModel::setEyeBlinkEnabled(bool enabled) {
|
|
209
|
+
std::lock_guard lock(_mutex);
|
|
210
|
+
_userModel->setEyeBlinkEnabled(enabled);
|
|
211
|
+
}
|
|
212
|
+
|
|
134
213
|
void HybridLive2DModel::focus(double x, double y) {
|
|
135
214
|
std::lock_guard lock(_mutex);
|
|
136
215
|
_userModel->focus(static_cast<float>(std::clamp(x, -1.0, 1.0)), static_cast<float>(std::clamp(y, -1.0, 1.0)));
|
|
@@ -26,11 +26,24 @@ class HybridLive2DModel : public HybridLive2DModelSpec {
|
|
|
26
26
|
void loadPose(const std::string& json) override;
|
|
27
27
|
void loadMotion(const std::string& group, double index, const std::string& json) override;
|
|
28
28
|
bool startMotion(const std::string& group, std::optional<double> index, std::optional<MotionPriority> priority) override;
|
|
29
|
+
void stopMotion() override;
|
|
30
|
+
bool isMotionPlaying() override;
|
|
31
|
+
void resetPose(std::optional<double> fadeSeconds) override;
|
|
29
32
|
void loadExpression(const std::string& name, const std::string& json) override;
|
|
30
33
|
bool setExpression(const std::optional<std::string>& name) override;
|
|
31
34
|
void clearExpression() override;
|
|
32
35
|
void setParameter(const std::string& id, double value) override;
|
|
33
36
|
void clearParameter(const std::string& id) override;
|
|
37
|
+
void setParameters(const std::vector<std::string>& ids, const std::vector<double>& values) override;
|
|
38
|
+
void clearParameters() override;
|
|
39
|
+
double getParameterSmoothing() override;
|
|
40
|
+
void setParameterSmoothing(double seconds) override;
|
|
41
|
+
bool getIdleMotionEnabled() override;
|
|
42
|
+
void setIdleMotionEnabled(bool enabled) override;
|
|
43
|
+
bool getEyeBlinkEnabled() override;
|
|
44
|
+
void setEyeBlinkEnabled(bool enabled) override;
|
|
45
|
+
std::vector<ParameterData> getParameters() override;
|
|
46
|
+
double getParameterValue(const std::string& id) override;
|
|
34
47
|
void focus(double x, double y) override;
|
|
35
48
|
std::vector<std::string> hitTest(double x, double y) override;
|
|
36
49
|
void update(double deltaSeconds) override;
|
|
@@ -20,16 +20,29 @@ namespace margelo::nitro::live2dskia {
|
|
|
20
20
|
prototype.registerHybridGetter("originY", &HybridLive2DModelSpec::getOriginY);
|
|
21
21
|
prototype.registerHybridGetter("pixelsPerUnit", &HybridLive2DModelSpec::getPixelsPerUnit);
|
|
22
22
|
prototype.registerHybridGetter("drawableCount", &HybridLive2DModelSpec::getDrawableCount);
|
|
23
|
+
prototype.registerHybridGetter("parameterSmoothing", &HybridLive2DModelSpec::getParameterSmoothing);
|
|
24
|
+
prototype.registerHybridSetter("parameterSmoothing", &HybridLive2DModelSpec::setParameterSmoothing);
|
|
25
|
+
prototype.registerHybridGetter("idleMotionEnabled", &HybridLive2DModelSpec::getIdleMotionEnabled);
|
|
26
|
+
prototype.registerHybridSetter("idleMotionEnabled", &HybridLive2DModelSpec::setIdleMotionEnabled);
|
|
27
|
+
prototype.registerHybridGetter("eyeBlinkEnabled", &HybridLive2DModelSpec::getEyeBlinkEnabled);
|
|
28
|
+
prototype.registerHybridSetter("eyeBlinkEnabled", &HybridLive2DModelSpec::setEyeBlinkEnabled);
|
|
23
29
|
prototype.registerHybridMethod("getDrawables", &HybridLive2DModelSpec::getDrawables);
|
|
24
30
|
prototype.registerHybridMethod("loadPhysics", &HybridLive2DModelSpec::loadPhysics);
|
|
25
31
|
prototype.registerHybridMethod("loadPose", &HybridLive2DModelSpec::loadPose);
|
|
26
32
|
prototype.registerHybridMethod("loadMotion", &HybridLive2DModelSpec::loadMotion);
|
|
27
33
|
prototype.registerHybridMethod("startMotion", &HybridLive2DModelSpec::startMotion);
|
|
34
|
+
prototype.registerHybridMethod("stopMotion", &HybridLive2DModelSpec::stopMotion);
|
|
35
|
+
prototype.registerHybridMethod("isMotionPlaying", &HybridLive2DModelSpec::isMotionPlaying);
|
|
36
|
+
prototype.registerHybridMethod("resetPose", &HybridLive2DModelSpec::resetPose);
|
|
28
37
|
prototype.registerHybridMethod("loadExpression", &HybridLive2DModelSpec::loadExpression);
|
|
29
38
|
prototype.registerHybridMethod("setExpression", &HybridLive2DModelSpec::setExpression);
|
|
30
39
|
prototype.registerHybridMethod("clearExpression", &HybridLive2DModelSpec::clearExpression);
|
|
31
40
|
prototype.registerHybridMethod("setParameter", &HybridLive2DModelSpec::setParameter);
|
|
32
41
|
prototype.registerHybridMethod("clearParameter", &HybridLive2DModelSpec::clearParameter);
|
|
42
|
+
prototype.registerHybridMethod("setParameters", &HybridLive2DModelSpec::setParameters);
|
|
43
|
+
prototype.registerHybridMethod("clearParameters", &HybridLive2DModelSpec::clearParameters);
|
|
44
|
+
prototype.registerHybridMethod("getParameters", &HybridLive2DModelSpec::getParameters);
|
|
45
|
+
prototype.registerHybridMethod("getParameterValue", &HybridLive2DModelSpec::getParameterValue);
|
|
33
46
|
prototype.registerHybridMethod("focus", &HybridLive2DModelSpec::focus);
|
|
34
47
|
prototype.registerHybridMethod("hitTest", &HybridLive2DModelSpec::hitTest);
|
|
35
48
|
prototype.registerHybridMethod("update", &HybridLive2DModelSpec::update);
|
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
namespace margelo::nitro::live2dskia { struct DrawableData; }
|
|
18
18
|
// Forward declaration of `MotionPriority` to properly resolve imports.
|
|
19
19
|
namespace margelo::nitro::live2dskia { enum class MotionPriority; }
|
|
20
|
+
// Forward declaration of `ParameterData` to properly resolve imports.
|
|
21
|
+
namespace margelo::nitro::live2dskia { struct ParameterData; }
|
|
20
22
|
|
|
21
23
|
#include "DrawableData.hpp"
|
|
22
24
|
#include <vector>
|
|
23
25
|
#include <string>
|
|
24
26
|
#include <optional>
|
|
25
27
|
#include "MotionPriority.hpp"
|
|
28
|
+
#include "ParameterData.hpp"
|
|
26
29
|
|
|
27
30
|
namespace margelo::nitro::live2dskia {
|
|
28
31
|
|
|
@@ -57,6 +60,12 @@ namespace margelo::nitro::live2dskia {
|
|
|
57
60
|
virtual double getOriginY() = 0;
|
|
58
61
|
virtual double getPixelsPerUnit() = 0;
|
|
59
62
|
virtual double getDrawableCount() = 0;
|
|
63
|
+
virtual double getParameterSmoothing() = 0;
|
|
64
|
+
virtual void setParameterSmoothing(double parameterSmoothing) = 0;
|
|
65
|
+
virtual bool getIdleMotionEnabled() = 0;
|
|
66
|
+
virtual void setIdleMotionEnabled(bool idleMotionEnabled) = 0;
|
|
67
|
+
virtual bool getEyeBlinkEnabled() = 0;
|
|
68
|
+
virtual void setEyeBlinkEnabled(bool eyeBlinkEnabled) = 0;
|
|
60
69
|
|
|
61
70
|
public:
|
|
62
71
|
// Methods
|
|
@@ -65,11 +74,18 @@ namespace margelo::nitro::live2dskia {
|
|
|
65
74
|
virtual void loadPose(const std::string& json) = 0;
|
|
66
75
|
virtual void loadMotion(const std::string& group, double index, const std::string& json) = 0;
|
|
67
76
|
virtual bool startMotion(const std::string& group, std::optional<double> index, std::optional<MotionPriority> priority) = 0;
|
|
77
|
+
virtual void stopMotion() = 0;
|
|
78
|
+
virtual bool isMotionPlaying() = 0;
|
|
79
|
+
virtual void resetPose(std::optional<double> fadeSeconds) = 0;
|
|
68
80
|
virtual void loadExpression(const std::string& name, const std::string& json) = 0;
|
|
69
81
|
virtual bool setExpression(const std::optional<std::string>& name) = 0;
|
|
70
82
|
virtual void clearExpression() = 0;
|
|
71
83
|
virtual void setParameter(const std::string& id, double value) = 0;
|
|
72
84
|
virtual void clearParameter(const std::string& id) = 0;
|
|
85
|
+
virtual void setParameters(const std::vector<std::string>& ids, const std::vector<double>& values) = 0;
|
|
86
|
+
virtual void clearParameters() = 0;
|
|
87
|
+
virtual std::vector<ParameterData> getParameters() = 0;
|
|
88
|
+
virtual double getParameterValue(const std::string& id) = 0;
|
|
73
89
|
virtual void focus(double x, double y) = 0;
|
|
74
90
|
virtual std::vector<std::string> hitTest(double x, double y) = 0;
|
|
75
91
|
virtual void update(double deltaSeconds) = 0;
|