@kubohiroya/turbowarp-realtime-motion-capture 0.3.0 → 0.4.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.
- package/README.ja.md +3 -3
- package/README.md +3 -3
- package/dist/extension-manifest.json +1 -1
- package/dist/turbowarp-realtime-motion-capture.js +138 -98
- package/docs/architecture.ja.md +21 -9
- package/docs/architecture.md +23 -12
- package/docs/index.html +4 -4
- package/docs/turbowarp-extension-api.ja.md +16 -16
- package/docs/turbowarp-extension-api.md +21 -23
- package/package.json +2 -2
- package/src/avatar/aframe-port.ts +34 -11
- package/src/avatar/controller.ts +112 -90
- package/src/avatar/types.ts +12 -11
- package/src/block-definitions.json +6 -6
- package/src/extension.ts +4 -4
package/README.ja.md
CHANGED
|
@@ -14,13 +14,13 @@ multiview pose estimationを使うcamera/fusion application向けの複合Turb
|
|
|
14
14
|
- WebGPU MoveNet MultiPoseで最大6人を追跡し、PoseFrame2D v1/v2 JSONを返します。
|
|
15
15
|
- このpackageが所有するmultiview-pose JSON contractを検証します。
|
|
16
16
|
- chessboardとOpenCV.jsで共有cameraをcalibrationします。
|
|
17
|
-
- 外部PoseFrame3D v1
|
|
17
|
+
- 外部PoseFrame3D v1をA-FrameのVRMアバターへretargetします。
|
|
18
18
|
- camera timingを計測し、同期した2D poseを3Dへ統合して、サイリウムで演者を識別します。
|
|
19
19
|
|
|
20
20
|
## 要件と安全性
|
|
21
21
|
|
|
22
22
|
- 信頼できるunsandboxed custom extensionを利用できるTurboWarp
|
|
23
|
-
- QR/時刻用WebRTC 0.3.0、video用Camera Source 0.5.0、avatar用A-Frame 0.
|
|
23
|
+
- QR/時刻用WebRTC 0.3.0、video用Camera Source 0.5.0、avatar用A-Frame 0.4.0
|
|
24
24
|
- 姿勢推定用TensorFlow.js WebGPUとcalibration用WebAssembly
|
|
25
25
|
|
|
26
26
|
全機能は起動時固定・既定OFFです。MoveNetにCPU/WASM/WebGL fallbackはなく、初回model loadは
|
|
@@ -37,7 +37,7 @@ custom extensionとして読み込みます。通常の順序はCamera Source、
|
|
|
37
37
|
version固定CDN URL:
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
|
-
https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-realtime-motion-capture@0.
|
|
40
|
+
https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-realtime-motion-capture@0.4.0/dist/turbowarp-realtime-motion-capture.js
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
npm packageが公開するのはbrowser向けstandalone bundle、schema、文書です。Composition APIは
|
package/README.md
CHANGED
|
@@ -14,13 +14,13 @@ Composite TurboWarp blocks for camera and fusion applications using multiview po
|
|
|
14
14
|
- Runs WebGPU MoveNet MultiPose for up to six people and reports PoseFrame2D v1/v2 JSON.
|
|
15
15
|
- Validates the owned multiview-pose JSON contracts.
|
|
16
16
|
- Calibrates shared cameras with a chessboard and OpenCV.js.
|
|
17
|
-
- Retargets external PoseFrame3D v1 data to
|
|
17
|
+
- Retargets external PoseFrame3D v1 data to VRM avatars in A-Frame.
|
|
18
18
|
- Measures camera timing, fuses synchronized 2D poses into 3D, and identifies performers by glow sticks.
|
|
19
19
|
|
|
20
20
|
## Requirements and safety
|
|
21
21
|
|
|
22
22
|
- TurboWarp with trusted unsandboxed custom extensions enabled.
|
|
23
|
-
- WebRTC 0.3.0 for QR/time, Camera Source 0.5.0 for video, and A-Frame 0.
|
|
23
|
+
- WebRTC 0.3.0 for QR/time, Camera Source 0.5.0 for video, and A-Frame 0.4.0 for avatars.
|
|
24
24
|
- TensorFlow.js WebGPU support for pose and WebAssembly support for calibration.
|
|
25
25
|
|
|
26
26
|
All features are startup-fixed and OFF by default. MoveNet has no CPU, WASM, or WebGL fallback and
|
|
@@ -35,7 +35,7 @@ extension. The usual order is Camera Source, WebRTC, A-Frame, then Realtime Moti
|
|
|
35
35
|
not used may be omitted. A version-pinned package URL is:
|
|
36
36
|
|
|
37
37
|
```text
|
|
38
|
-
https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-realtime-motion-capture@0.
|
|
38
|
+
https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-realtime-motion-capture@0.4.0/dist/turbowarp-realtime-motion-capture.js
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
The npm package distributes a standalone browser bundle, schemas, and documentation. It does not
|
|
@@ -541,20 +541,20 @@
|
|
|
541
541
|
"opcode": "registerAvatarAsset",
|
|
542
542
|
"feature": "avatarRetargetV1",
|
|
543
543
|
"blockType": "COMMAND",
|
|
544
|
-
"text": "register avatar asset [ASSET_ID]
|
|
545
|
-
"description": "Registers
|
|
544
|
+
"text": "register avatar asset [ASSET_ID] VRM [VRM_URL] rig JSON [RIG_JSON]",
|
|
545
|
+
"description": "Registers a VRM model and its root placement for avatars driven through TurboWarp-A-Frame capability v2.",
|
|
546
546
|
"arguments": {
|
|
547
547
|
"ASSET_ID": {
|
|
548
548
|
"type": "STRING",
|
|
549
549
|
"defaultValue": "actor"
|
|
550
550
|
},
|
|
551
|
-
"
|
|
551
|
+
"VRM_URL": {
|
|
552
552
|
"type": "STRING",
|
|
553
|
-
"defaultValue": "
|
|
553
|
+
"defaultValue": "avatar.vrm"
|
|
554
554
|
},
|
|
555
555
|
"RIG_JSON": {
|
|
556
556
|
"type": "STRING",
|
|
557
|
-
"defaultValue": "{\"
|
|
557
|
+
"defaultValue": "{\"rootScale\":1,\"rootOffset\":[0,0,0]}"
|
|
558
558
|
}
|
|
559
559
|
}
|
|
560
560
|
},
|
|
@@ -563,7 +563,7 @@
|
|
|
563
563
|
"feature": "avatarRetargetV1",
|
|
564
564
|
"blockType": "COMMAND",
|
|
565
565
|
"text": "bind person [PERSON_ID] to avatar [INSTANCE_ID] asset [ASSET_ID] under [PARENT] confidence [CONFIDENCE]",
|
|
566
|
-
"description": "Creates an avatar instance and binds one PoseFrame3D person ID to it.",
|
|
566
|
+
"description": "Creates an avatar instance, loads its VRM, and binds one PoseFrame3D person ID to it. The block waits until the VRM is ready.",
|
|
567
567
|
"arguments": {
|
|
568
568
|
"PERSON_ID": {
|
|
569
569
|
"type": "STRING",
|
|
@@ -78248,19 +78248,33 @@
|
|
|
78248
78248
|
"loadTemplate",
|
|
78249
78249
|
"createFromTemplate",
|
|
78250
78250
|
"setPosition",
|
|
78251
|
-
"setRotation",
|
|
78252
78251
|
"emitEvent",
|
|
78253
78252
|
"deleteSelector",
|
|
78254
78253
|
"countSelector",
|
|
78254
|
+
"loadVrm",
|
|
78255
|
+
"setVrmBoneRotation",
|
|
78255
78256
|
"requireVersion"
|
|
78256
78257
|
];
|
|
78257
|
-
|
|
78258
|
+
/**
|
|
78259
|
+
* Returns TurboWarp-A-Frame capability v2, the only version avatar retargeting accepts.
|
|
78260
|
+
* A build that cannot provide v2 is refused rather than driven through an older contract.
|
|
78261
|
+
*/
|
|
78262
|
+
function requireAFrameCapability(runtime) {
|
|
78258
78263
|
const candidate = runtime[AFRAME_CAPABILITY_KEY];
|
|
78259
|
-
if (typeof candidate !== "object" || candidate === null) throw new Error("TurboWarp-A-Frame capability
|
|
78260
|
-
|
|
78261
|
-
|
|
78262
|
-
|
|
78263
|
-
|
|
78264
|
+
if (typeof candidate !== "object" || candidate === null) throw new Error("TurboWarp-A-Frame capability v2 must be loaded before avatar retargeting.");
|
|
78265
|
+
const requireVersion = Reflect.get(candidate, "requireVersion");
|
|
78266
|
+
if (typeof requireVersion !== "function") throw new Error("TurboWarp-A-Frame capability v2 is missing requireVersion().");
|
|
78267
|
+
let capability;
|
|
78268
|
+
try {
|
|
78269
|
+
capability = requireVersion.call(candidate, 2);
|
|
78270
|
+
} catch (error) {
|
|
78271
|
+
throw new Error(`TurboWarp-A-Frame capability v2 is required: ${error instanceof Error ? error.message : String(error)}`);
|
|
78272
|
+
}
|
|
78273
|
+
if (typeof capability !== "object" || capability === null) throw new Error("TurboWarp-A-Frame capability v2 is required.");
|
|
78274
|
+
for (const method of METHODS) if (typeof Reflect.get(capability, method) !== "function") throw new Error(`TurboWarp-A-Frame capability v2 is missing ${method}().`);
|
|
78275
|
+
const version = Reflect.get(capability, "version");
|
|
78276
|
+
if (version !== 2) throw new Error(`TurboWarp-A-Frame capability v2 is required; found version ${String(version)}.`);
|
|
78277
|
+
return capability;
|
|
78264
78278
|
}
|
|
78265
78279
|
//#endregion
|
|
78266
78280
|
//#region node_modules/.pnpm/kalidokit@1.1.5/node_modules/kalidokit/dist/utils/helpers.js
|
|
@@ -79815,18 +79829,42 @@
|
|
|
79815
79829
|
//#region src/avatar/controller.ts
|
|
79816
79830
|
var identifiers = /^[A-Za-z0-9._-]{1,64}$/u;
|
|
79817
79831
|
var eventNames = /^[A-Za-z0-9._:-]{1,80}$/u;
|
|
79818
|
-
|
|
79832
|
+
/** Every avatar instance is an empty node that the VRM is loaded onto. */
|
|
79833
|
+
var HOLDER_TEMPLATE = JSON.stringify({ type: "empty" });
|
|
79834
|
+
/**
|
|
79835
|
+
* Kalidokit solves a mirrored selfie view: its `Right*` outputs come from the performer's
|
|
79836
|
+
* left landmarks. A stage avatar moves the performer's own side, so each output drives the
|
|
79837
|
+
* opposite VRM humanoid bone.
|
|
79838
|
+
*/
|
|
79839
|
+
var VRM_BONES = {
|
|
79840
|
+
RightUpperArm: "leftUpperArm",
|
|
79841
|
+
RightLowerArm: "leftLowerArm",
|
|
79842
|
+
LeftUpperArm: "rightUpperArm",
|
|
79843
|
+
LeftLowerArm: "rightLowerArm",
|
|
79844
|
+
RightHand: "leftHand",
|
|
79845
|
+
LeftHand: "rightHand",
|
|
79846
|
+
RightUpperLeg: "leftUpperLeg",
|
|
79847
|
+
RightLowerLeg: "leftLowerLeg",
|
|
79848
|
+
LeftUpperLeg: "rightUpperLeg",
|
|
79849
|
+
LeftLowerLeg: "rightLowerLeg",
|
|
79850
|
+
Spine: "spine",
|
|
79851
|
+
Hips: "hips"
|
|
79852
|
+
};
|
|
79853
|
+
/**
|
|
79854
|
+
* The joints each Kalidokit output is computed from. Its `Right*` outputs read the
|
|
79855
|
+
* performer's left landmarks (BlazePose 11, 13, 15, 23, 25, 27), and `Left*` the right.
|
|
79856
|
+
*/
|
|
79819
79857
|
var RIG_REQUIRED_JOINTS = {
|
|
79820
|
-
RightUpperArm: ["
|
|
79821
|
-
RightLowerArm: ["
|
|
79822
|
-
LeftUpperArm: ["
|
|
79823
|
-
LeftLowerArm: ["
|
|
79824
|
-
RightHand: ["
|
|
79825
|
-
LeftHand: ["
|
|
79826
|
-
RightUpperLeg: ["
|
|
79827
|
-
RightLowerLeg: ["
|
|
79828
|
-
LeftUpperLeg: ["
|
|
79829
|
-
LeftLowerLeg: ["
|
|
79858
|
+
RightUpperArm: ["left_shoulder", "left_elbow"],
|
|
79859
|
+
RightLowerArm: ["left_elbow", "left_wrist"],
|
|
79860
|
+
LeftUpperArm: ["right_shoulder", "right_elbow"],
|
|
79861
|
+
LeftLowerArm: ["right_elbow", "right_wrist"],
|
|
79862
|
+
RightHand: ["left_wrist"],
|
|
79863
|
+
LeftHand: ["right_wrist"],
|
|
79864
|
+
RightUpperLeg: ["left_hip", "left_knee"],
|
|
79865
|
+
RightLowerLeg: ["left_knee", "left_ankle"],
|
|
79866
|
+
LeftUpperLeg: ["right_hip", "right_knee"],
|
|
79867
|
+
LeftLowerLeg: ["right_knee", "right_ankle"],
|
|
79830
79868
|
Spine: [
|
|
79831
79869
|
"left_shoulder",
|
|
79832
79870
|
"right_shoulder",
|
|
@@ -79845,21 +79883,23 @@
|
|
|
79845
79883
|
this.runtime = runtime;
|
|
79846
79884
|
this.solver = solver;
|
|
79847
79885
|
}
|
|
79848
|
-
registerAsset(assetIdValue,
|
|
79886
|
+
registerAsset(assetIdValue, vrmUrlValue, rigJson) {
|
|
79849
79887
|
const assetId = identifier(assetIdValue, "avatar asset ID");
|
|
79850
|
-
|
|
79888
|
+
const vrmUrl = nonEmpty(vrmUrlValue, "VRM URL");
|
|
79889
|
+
if (vrmUrl.length > 2048) throw new Error("VRM URL exceeds 2048 characters.");
|
|
79851
79890
|
const rig = parseRigMapping(rigJson);
|
|
79852
|
-
const aframe =
|
|
79891
|
+
const aframe = requireAFrameCapability(this.runtime);
|
|
79853
79892
|
const templateId = `twmp-avatar-${assetId}`;
|
|
79854
|
-
aframe.loadTemplate(templateId,
|
|
79893
|
+
aframe.loadTemplate(templateId, HOLDER_TEMPLATE);
|
|
79855
79894
|
this.assets.set(assetId, {
|
|
79856
79895
|
id: assetId,
|
|
79857
79896
|
templateId,
|
|
79897
|
+
vrmUrl,
|
|
79858
79898
|
rig
|
|
79859
79899
|
});
|
|
79860
79900
|
this.succeed("configured");
|
|
79861
79901
|
}
|
|
79862
|
-
bind(personIdValue, instanceIdValue, assetIdValue, parentSelectorValue, confidenceValue) {
|
|
79902
|
+
async bind(personIdValue, instanceIdValue, assetIdValue, parentSelectorValue, confidenceValue) {
|
|
79863
79903
|
const personId = identifier(personIdValue, "person ID");
|
|
79864
79904
|
const instanceId = identifier(instanceIdValue, "avatar instance ID");
|
|
79865
79905
|
const assetId = identifier(assetIdValue, "avatar asset ID");
|
|
@@ -79871,25 +79911,41 @@
|
|
|
79871
79911
|
const existingInstance = [...this.bindings.values()].find((binding) => binding.instanceId === instanceId);
|
|
79872
79912
|
if (existingInstance && existingInstance.personId !== personId) throw new Error(`Avatar instance is already bound: ${instanceId}`);
|
|
79873
79913
|
if (!existingPerson && this.bindings.size >= 6) throw new Error("Avatar retargeting supports at most six people.");
|
|
79874
|
-
const aframe =
|
|
79914
|
+
const aframe = requireAFrameCapability(this.runtime);
|
|
79875
79915
|
if (instanceId !== existingPerson?.instanceId && aframe.countSelector(`#${instanceId}`) > 0) throw new Error(`A-Frame node already exists: ${instanceId}`);
|
|
79876
79916
|
if (existingPerson) this.removeBinding(aframe, existingPerson);
|
|
79877
79917
|
if (aframe.countSelector(`#${instanceId}`) > 0) throw new Error(`A-Frame node already exists: ${instanceId}`);
|
|
79878
79918
|
aframe.createFromTemplate(asset.templateId, instanceId, parentSelector);
|
|
79879
|
-
|
|
79919
|
+
const binding = {
|
|
79880
79920
|
personId,
|
|
79881
79921
|
instanceId,
|
|
79882
79922
|
assetId,
|
|
79883
79923
|
confidence,
|
|
79884
|
-
recognized: false
|
|
79885
|
-
|
|
79924
|
+
recognized: false,
|
|
79925
|
+
loaded: false
|
|
79926
|
+
};
|
|
79927
|
+
this.bindings.set(personId, binding);
|
|
79928
|
+
this.lastState = "loading";
|
|
79929
|
+
try {
|
|
79930
|
+
await aframe.loadVrm(asset.vrmUrl, `#${instanceId}`);
|
|
79931
|
+
} catch (error) {
|
|
79932
|
+
if (this.bindings.get(personId) === binding) {
|
|
79933
|
+
this.bindings.delete(personId);
|
|
79934
|
+
aframe.deleteSelector(`#${instanceId}`);
|
|
79935
|
+
this.lastState = "error";
|
|
79936
|
+
this.lastError = `${personId}: ${message(error)}`;
|
|
79937
|
+
}
|
|
79938
|
+
throw error;
|
|
79939
|
+
}
|
|
79940
|
+
if (this.bindings.get(personId) !== binding) return;
|
|
79941
|
+
binding.loaded = true;
|
|
79886
79942
|
this.succeed("bound");
|
|
79887
79943
|
}
|
|
79888
79944
|
unbind(personIdValue) {
|
|
79889
79945
|
const personId = identifier(personIdValue, "person ID");
|
|
79890
79946
|
const binding = this.bindings.get(personId);
|
|
79891
79947
|
if (!binding) return;
|
|
79892
|
-
this.removeBinding(
|
|
79948
|
+
this.removeBinding(requireAFrameCapability(this.runtime), binding);
|
|
79893
79949
|
this.succeed("configured");
|
|
79894
79950
|
}
|
|
79895
79951
|
apply(frameJson, pose2dJson) {
|
|
@@ -79901,7 +79957,7 @@
|
|
|
79901
79957
|
try {
|
|
79902
79958
|
frame = parseFrame(frameJson);
|
|
79903
79959
|
pose2d = parsePoseFrame2D(pose2dJson);
|
|
79904
|
-
aframe =
|
|
79960
|
+
aframe = requireAFrameCapability(this.runtime);
|
|
79905
79961
|
} catch (error) {
|
|
79906
79962
|
this.lastState = "error";
|
|
79907
79963
|
this.lastError = message(error);
|
|
@@ -79910,23 +79966,26 @@
|
|
|
79910
79966
|
const people = new Map(frame.persons.map((person) => [person.personId, person]));
|
|
79911
79967
|
const screenPeople = new Map(pose2d.persons.map((person) => [person.trackingId, person]));
|
|
79912
79968
|
const errors = [];
|
|
79913
|
-
for (const binding of [...this.bindings.values()])
|
|
79914
|
-
if (
|
|
79915
|
-
|
|
79916
|
-
|
|
79917
|
-
|
|
79918
|
-
|
|
79919
|
-
|
|
79920
|
-
|
|
79921
|
-
|
|
79922
|
-
|
|
79923
|
-
|
|
79969
|
+
for (const binding of [...this.bindings.values()]) {
|
|
79970
|
+
if (!binding.loaded) continue;
|
|
79971
|
+
try {
|
|
79972
|
+
if (aframe.countSelector(`#${binding.instanceId}`) !== 1) {
|
|
79973
|
+
this.bindings.delete(binding.personId);
|
|
79974
|
+
errors.push(`${binding.personId}: avatar instance is missing after scene reset`);
|
|
79975
|
+
continue;
|
|
79976
|
+
}
|
|
79977
|
+
const person = people.get(binding.personId);
|
|
79978
|
+
const screenPerson = screenPeople.get(binding.personId);
|
|
79979
|
+
if (!person || !screenPerson || person.score < binding.confidence || screenPerson.score < binding.confidence) {
|
|
79980
|
+
this.setRecognized(aframe, binding, false, frame.timestampUs);
|
|
79981
|
+
continue;
|
|
79982
|
+
}
|
|
79983
|
+
this.applyPerson(aframe, binding, person, screenPerson, pose2d);
|
|
79984
|
+
this.setRecognized(aframe, binding, true, frame.timestampUs);
|
|
79985
|
+
this.lastUpdated += 1;
|
|
79986
|
+
} catch (error) {
|
|
79987
|
+
errors.push(`${binding.personId}: ${message(error)}`);
|
|
79924
79988
|
}
|
|
79925
|
-
this.applyPerson(aframe, binding, person, screenPerson, pose2d);
|
|
79926
|
-
this.setRecognized(aframe, binding, true, frame.timestampUs);
|
|
79927
|
-
this.lastUpdated += 1;
|
|
79928
|
-
} catch (error) {
|
|
79929
|
-
errors.push(`${binding.personId}: ${message(error)}`);
|
|
79930
79989
|
}
|
|
79931
79990
|
if (errors.length > 0) {
|
|
79932
79991
|
this.lastState = "partial";
|
|
@@ -79936,7 +79995,7 @@
|
|
|
79936
79995
|
reset() {
|
|
79937
79996
|
const candidate = this.runtime.turbowarpAFrameCapability;
|
|
79938
79997
|
if (typeof candidate === "object" && candidate !== null) try {
|
|
79939
|
-
const aframe =
|
|
79998
|
+
const aframe = requireAFrameCapability(this.runtime);
|
|
79940
79999
|
for (const binding of [...this.bindings.values()]) this.removeBinding(aframe, binding);
|
|
79941
80000
|
} catch {
|
|
79942
80001
|
this.bindings.clear();
|
|
@@ -79973,15 +80032,12 @@
|
|
|
79973
80032
|
const [offsetX, offsetY, offsetZ] = asset.rig.rootOffset;
|
|
79974
80033
|
aframe.setPosition(`#${binding.instanceId}`, root.x * asset.rig.rootScale + offsetX, root.y * asset.rig.rootScale + offsetY, root.z * asset.rig.rootScale + offsetZ);
|
|
79975
80034
|
}
|
|
79976
|
-
for (const
|
|
80035
|
+
for (const key of KALIDOKIT_RIG_KEYS) this.applyBone(aframe, binding, key, rig, worldKeypoints, screenKeypoints);
|
|
79977
80036
|
}
|
|
79978
|
-
applyBone(aframe, binding,
|
|
79979
|
-
if (!requiredJoints(
|
|
79980
|
-
const
|
|
79981
|
-
|
|
79982
|
-
const rotation = rotationForRig(rig, bone.rig);
|
|
79983
|
-
const [offsetX, offsetY, offsetZ] = bone.offsetDegrees;
|
|
79984
|
-
aframe.setRotation(selector, radiansToDegrees(rotation.x) + offsetX, radiansToDegrees(rotation.y) + offsetY, radiansToDegrees(rotation.z) + offsetZ);
|
|
80037
|
+
applyBone(aframe, binding, key, rig, worldKeypoints, screenKeypoints) {
|
|
80038
|
+
if (!requiredJoints(key).every((id) => (worldKeypoints.get(id)?.score ?? 0) >= binding.confidence && (screenKeypoints.get(id)?.score ?? 0) >= binding.confidence)) return;
|
|
80039
|
+
const [x, y, z] = vrmBoneDegrees(rotationForRig(rig, key));
|
|
80040
|
+
aframe.setVrmBoneRotation(`#${binding.instanceId}`, VRM_BONES[key], x, y, z);
|
|
79985
80041
|
}
|
|
79986
80042
|
setRecognized(aframe, binding, recognized, timestampUs) {
|
|
79987
80043
|
if (binding.recognized === recognized) return;
|
|
@@ -80022,48 +80078,24 @@
|
|
|
80022
80078
|
}
|
|
80023
80079
|
function parseRigMapping(source) {
|
|
80024
80080
|
const value = parseJsonObject(source, "Avatar rig mapping JSON");
|
|
80081
|
+
if ("bones" in value) throw new Error("Rig mapping no longer takes bones: VRM humanoid bones are driven directly.");
|
|
80025
80082
|
rejectUnknownKeys(value, /* @__PURE__ */ new Set([
|
|
80026
80083
|
"rootScale",
|
|
80027
80084
|
"rootOffset",
|
|
80028
80085
|
"recognitionStartEvent",
|
|
80029
|
-
"recognitionEndEvent"
|
|
80030
|
-
"bones"
|
|
80086
|
+
"recognitionEndEvent"
|
|
80031
80087
|
]), "rig mapping");
|
|
80032
80088
|
const rootScale = finite(value.rootScale ?? 1, "rootScale");
|
|
80033
80089
|
if (rootScale <= 0) throw new Error("rootScale must be greater than zero.");
|
|
80034
|
-
const rootOffset = vector3(value.rootOffset ?? [
|
|
80035
|
-
0,
|
|
80036
|
-
0,
|
|
80037
|
-
0
|
|
80038
|
-
], "rootOffset");
|
|
80039
|
-
const recognitionStartEvent = eventName(value.recognitionStartEvent ?? "twmp-recognition-start", "recognitionStartEvent");
|
|
80040
|
-
const recognitionEndEvent = eventName(value.recognitionEndEvent ?? "twmp-recognition-end", "recognitionEndEvent");
|
|
80041
|
-
if (!Array.isArray(value.bones) || value.bones.length === 0 || value.bones.length > 32) throw new Error("Rig mapping bones must contain between 1 and 32 entries.");
|
|
80042
80090
|
return {
|
|
80043
80091
|
rootScale,
|
|
80044
|
-
rootOffset
|
|
80045
|
-
recognitionStartEvent,
|
|
80046
|
-
recognitionEndEvent,
|
|
80047
|
-
bones: value.bones.map((entry, index) => parseBone(entry, index))
|
|
80048
|
-
};
|
|
80049
|
-
}
|
|
80050
|
-
function parseBone(value, index) {
|
|
80051
|
-
const bone = record(value, `bones[${index}]`);
|
|
80052
|
-
rejectUnknownKeys(bone, /* @__PURE__ */ new Set([
|
|
80053
|
-
"selector",
|
|
80054
|
-
"rig",
|
|
80055
|
-
"offsetDegrees"
|
|
80056
|
-
]), `bones[${index}]`);
|
|
80057
|
-
const selector = nonEmpty(bone.selector, `bones[${index}].selector`);
|
|
80058
|
-
if (!selector.includes("{avatar}")) throw new Error(`bones[${index}].selector must contain {avatar}.`);
|
|
80059
|
-
return {
|
|
80060
|
-
selector,
|
|
80061
|
-
rig: rigKey(bone.rig, `bones[${index}].rig`),
|
|
80062
|
-
offsetDegrees: vector3(bone.offsetDegrees ?? [
|
|
80092
|
+
rootOffset: vector3(value.rootOffset ?? [
|
|
80063
80093
|
0,
|
|
80064
80094
|
0,
|
|
80065
80095
|
0
|
|
80066
|
-
],
|
|
80096
|
+
], "rootOffset"),
|
|
80097
|
+
recognitionStartEvent: eventName(value.recognitionStartEvent ?? "twmp-recognition-start", "recognitionStartEvent"),
|
|
80098
|
+
recognitionEndEvent: eventName(value.recognitionEndEvent ?? "twmp-recognition-end", "recognitionEndEvent")
|
|
80067
80099
|
};
|
|
80068
80100
|
}
|
|
80069
80101
|
function parseJsonObject(source, label) {
|
|
@@ -80083,10 +80115,6 @@
|
|
|
80083
80115
|
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
80084
80116
|
if (unknown) throw new Error(`${label} contains unknown field: ${unknown}`);
|
|
80085
80117
|
}
|
|
80086
|
-
function rigKey(value, label) {
|
|
80087
|
-
if (typeof value !== "string" || !KALIDOKIT_RIG_KEY_SET.has(value)) throw new Error(`${label} must be a supported Kalidokit pose rig key.`);
|
|
80088
|
-
return value;
|
|
80089
|
-
}
|
|
80090
80118
|
function vector3(value, label) {
|
|
80091
80119
|
if (!Array.isArray(value) || value.length !== 3) throw new Error(`${label} must contain exactly three numbers.`);
|
|
80092
80120
|
return [
|
|
@@ -80117,8 +80145,20 @@
|
|
|
80117
80145
|
if (!eventNames.test(normalized)) throw new Error(`Invalid ${label}.`);
|
|
80118
80146
|
return normalized;
|
|
80119
80147
|
}
|
|
80120
|
-
|
|
80121
|
-
|
|
80148
|
+
/**
|
|
80149
|
+
* Converts a Kalidokit rotation in radians to Euler degrees on a VRM normalized bone.
|
|
80150
|
+
*
|
|
80151
|
+
* Kalidokit's rotations are for VRM 0.x bone axes, which face -Z; normalized bones use the
|
|
80152
|
+
* VRM 1.0 axes, a half turn about Y away, which negates x and z. Driving the opposite side
|
|
80153
|
+
* (see VRM_BONES) also mirrors the rotation across the body's midline, which negates y and z.
|
|
80154
|
+
* Together the signs become (-x, -y, z).
|
|
80155
|
+
*/
|
|
80156
|
+
function vrmBoneDegrees(rotation) {
|
|
80157
|
+
return [
|
|
80158
|
+
-radiansToDegrees(rotation.x),
|
|
80159
|
+
-radiansToDegrees(rotation.y),
|
|
80160
|
+
radiansToDegrees(rotation.z)
|
|
80161
|
+
];
|
|
80122
80162
|
}
|
|
80123
80163
|
function radiansToDegrees(value) {
|
|
80124
80164
|
return value * 180 / Math.PI;
|
|
@@ -82954,11 +82994,11 @@
|
|
|
82954
82994
|
}
|
|
82955
82995
|
registerAvatarAsset(args) {
|
|
82956
82996
|
this.requireAvatarEnabled();
|
|
82957
|
-
this.avatar.registerAsset(Scratch.Cast.toString(args.ASSET_ID), Scratch.Cast.toString(args.
|
|
82997
|
+
this.avatar.registerAsset(Scratch.Cast.toString(args.ASSET_ID), Scratch.Cast.toString(args.VRM_URL), Scratch.Cast.toString(args.RIG_JSON));
|
|
82958
82998
|
}
|
|
82959
82999
|
bindAvatarPerson(args) {
|
|
82960
83000
|
this.requireAvatarEnabled();
|
|
82961
|
-
this.avatar.bind(Scratch.Cast.toString(args.PERSON_ID), Scratch.Cast.toString(args.INSTANCE_ID), Scratch.Cast.toString(args.ASSET_ID), Scratch.Cast.toString(args.PARENT), Scratch.Cast.toNumber(args.CONFIDENCE));
|
|
83001
|
+
return this.avatar.bind(Scratch.Cast.toString(args.PERSON_ID), Scratch.Cast.toString(args.INSTANCE_ID), Scratch.Cast.toString(args.ASSET_ID), Scratch.Cast.toString(args.PARENT), Scratch.Cast.toNumber(args.CONFIDENCE));
|
|
82962
83002
|
}
|
|
82963
83003
|
unbindAvatarPerson(args) {
|
|
82964
83004
|
this.requireAvatarEnabled();
|
package/docs/architecture.ja.md
CHANGED
|
@@ -134,23 +134,35 @@ detectorのdisposeで解放します。
|
|
|
134
134
|
## PoseFrame3D avatar retarget
|
|
135
135
|
|
|
136
136
|
`avatarRetargetV1`は独立した起動時固定・既定OFF flagです。runtime key
|
|
137
|
-
`turbowarpAFrameCapability`へ`requireVersion(
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
`turbowarpAFrameCapability`へ`requireVersion(2)`を呼び、それ以外のversionは受け付けません。
|
|
138
|
+
capability v2を持たないA-Frame(0.3.0など)は拒否します。そのcapabilityのscene操作とVRM操作だけを
|
|
139
|
+
利用し、A-Frame DOM、Three.js object、GLTF内部へはアクセスしません。capability v2は
|
|
140
|
+
`@kubohiroya/turbowarp-aframe@0.4.0`で公開します。
|
|
140
141
|
|
|
141
|
-
asset
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
PoseFrame2Dの`trackingId`が一致するperson
|
|
142
|
+
asset登録ではVRMのURLと検証済みのroot配置を保持します。personをbindすると、holder templateから
|
|
143
|
+
空のnodeを作ってVRMを読み込み、準備ができるまで待ちます。読み込みに失敗したらnodeとbindingを
|
|
144
|
+
取り除きます。適用時は対応するexact-v1 PoseFrame3DとPoseFrame2Dの両方を要求します。
|
|
145
|
+
PoseFrame3Dの`personId`とPoseFrame2Dの`trackingId`が一致するpersonだけを結合しますが、これは
|
|
146
|
+
時刻alignmentではありません。
|
|
145
147
|
|
|
146
148
|
adapterは両方のCOCO-17 recordを、exact pinした`kalidokit@1.1.5`が要求する33 positionへ
|
|
147
149
|
決定論的に変換します。screen座標にはPoseFrame2Dの`frameWidth`/`frameHeight`を使い、world座標は
|
|
148
150
|
外部serviceの値を維持します。不足するBlazePose face/hand/foot pointは低visibilityで中点補間
|
|
149
151
|
または複製します。`runtime: "tfjs"`、`enableLegs: true`のKalidokit `Pose.solve`だけをrotation
|
|
150
|
-
solverとし、
|
|
152
|
+
solverとし、hips結果にroot scale/offsetを適用し、
|
|
151
153
|
自前rotation fallbackは持ちません。joint/personがbinding threshold未満なら該当transformだけを
|
|
152
154
|
skipし、直前値を維持します。
|
|
153
155
|
|
|
156
|
+
Kalidokitは、VRM 0.xのボーンの軸で、鏡に映した自撮りの視点を解きます。`Right*`の出力は演者の
|
|
157
|
+
左のlandmarkから来るため、各出力は反対側のVRMヒューマノイドのボーンを回し、演者自身の側を
|
|
158
|
+
動かします。回転は度で正規化されたボーンへ書きます。正規化されたボーンはVRM 0.xからYまわりに
|
|
159
|
+
半回転したVRM 1.0の軸を使うためxとzの符号が反転し、反対側を動かすことで正中面について鏡映する
|
|
160
|
+
ためyとzの符号が反転します。合わせて符号は(-x, -y, z)になります。これはKalidokit 1.1.5と
|
|
161
|
+
VRMに合成の姿勢を入れて確かめました。腕を30°下ろす、片腕を水平に保つ、膝を上げる、前腕を前へ
|
|
162
|
+
伸ばす、のいずれも演者自身の手足が同じ向きに動きました。Kalidokitの腕の出力は、水平より上げた
|
|
163
|
+
腕と下げた腕をほとんど区別せず、体を傾けてもspineの傾きを返さなかったため、spineとhipsの符号は
|
|
164
|
+
同じ導出によるもので、まだ観測していません。
|
|
165
|
+
|
|
154
166
|
最大6 person IDを一意なtemplate instanceへbindします。recognition遷移は設定可能なA-Frame
|
|
155
167
|
eventで通知し、application側がPerformance DSLのstart/end effectへ接続できます。1人の
|
|
156
168
|
capability失敗は`partial`診断へ集約し、他avatarを継続します。rebind、明示reset、project
|
|
@@ -159,7 +171,7 @@ lifecycle reset、disposeでは可能ならend eventを送り、生成instance
|
|
|
159
171
|
PoseFrame3Dは別実装の3D serviceから届くexact v1境界dataです。`timestampUs`は不透明値として
|
|
160
172
|
recognition event dataへcopyするだけです。frame alignment、履歴保持/query、triangulation、
|
|
161
173
|
3D solveは行いません。Kalidokitは上流でdeprecatedでありnative BlazePose landmarkを想定するため、
|
|
162
|
-
このCOCO-17拡張は明示的な精度制約です。release
|
|
174
|
+
このCOCO-17拡張は明示的な精度制約です。release前に実際の記録でretargetを検証します。
|
|
163
175
|
|
|
164
176
|
## フレーム同期パターンの縦切り
|
|
165
177
|
|
package/docs/architecture.md
CHANGED
|
@@ -154,23 +154,34 @@ camera/board geometry and WebAssembly startup remain browser E2E responsibilitie
|
|
|
154
154
|
## PoseFrame3D avatar retargeting
|
|
155
155
|
|
|
156
156
|
`avatarRetargetV1` is an independent startup-fixed, default-OFF flag. It requires runtime key
|
|
157
|
-
`turbowarpAFrameCapability
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
`@kubohiroya/turbowarp-aframe@0.
|
|
157
|
+
`turbowarpAFrameCapability` and calls `requireVersion(2)`; it accepts no other version, so an
|
|
158
|
+
A-Frame build without capability v2, such as 0.3.0, is refused. It uses the public scene
|
|
159
|
+
operations and the VRM operations of that capability, and never accesses A-Frame DOM, Three.js
|
|
160
|
+
objects, or GLTF internals. Capability v2 is published in `@kubohiroya/turbowarp-aframe@0.4.0`.
|
|
161
161
|
|
|
162
|
-
An asset registration
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
PoseFrame2D values. A person is joined only by PoseFrame3D `personId` equal to
|
|
166
|
-
`trackingId`; this is not temporal alignment.
|
|
162
|
+
An asset registration records a VRM URL and a validated root placement. Binding a person creates
|
|
163
|
+
an empty node from a holder template, loads the VRM onto it, and waits until it is ready; a failed
|
|
164
|
+
load removes the node and the binding. Applying a frame requires corresponding exact-v1
|
|
165
|
+
PoseFrame3D and PoseFrame2D values. A person is joined only by PoseFrame3D `personId` equal to
|
|
166
|
+
PoseFrame2D `trackingId`; this is not temporal alignment.
|
|
167
167
|
|
|
168
168
|
The adapter maps both COCO-17 records deterministically to the 33 positions required by
|
|
169
169
|
exact-pinned `kalidokit@1.1.5`. Screen coordinates use PoseFrame2D `frameWidth` and `frameHeight`;
|
|
170
170
|
world coordinates retain the external service coordinate values. Missing BlazePose face, hand, and
|
|
171
171
|
foot points are midpoint-interpolated or duplicated with reduced visibility. Kalidokit `Pose.solve`
|
|
172
|
-
with `runtime: "tfjs"` and `enableLegs: true` is the sole rotation solver. Its
|
|
173
|
-
|
|
172
|
+
with `runtime: "tfjs"` and `enableLegs: true` is the sole rotation solver. Its hips result drives
|
|
173
|
+
the configured root scale and offset.
|
|
174
|
+
|
|
175
|
+
Kalidokit solves a mirrored selfie view for VRM 0.x bone axes. Its `Right*` outputs come from the
|
|
176
|
+
performer's left landmarks, so each output turns the opposite VRM humanoid bone, which moves the
|
|
177
|
+
performer's own side. The rotation is written in degrees to the normalized bone: normalized bones
|
|
178
|
+
use the VRM 1.0 axes, a half turn about Y from VRM 0.x, which negates x and z, and driving the
|
|
179
|
+
opposite side mirrors the rotation across the midline, which negates y and z; together the signs
|
|
180
|
+
become (-x, -y, z). This was checked on a VRM with Kalidokit 1.1.5 and synthetic poses: an arm
|
|
181
|
+
lowered 30°, one arm held level, a raised knee, and a forearm reaching forward each moved the
|
|
182
|
+
performer's own limb in the same direction. Kalidokit's arm output barely separates an arm raised
|
|
183
|
+
above level from one lowered below it, and it reported no spine lean for a leaning torso, so the
|
|
184
|
+
spine and hips signs follow from the same derivation but are not yet observed. There is no
|
|
174
185
|
custom rotation fallback. Joint or person confidence below the binding threshold skips only that
|
|
175
186
|
transform and preserves its prior value.
|
|
176
187
|
|
|
@@ -184,7 +195,7 @@ PoseFrame3D is exact v1 boundary data from a separate 3D service. Its `timestamp
|
|
|
184
195
|
only copied into recognition event data. This extension performs no frame alignment, history
|
|
185
196
|
retention/query, triangulation, or 3D solve. Kalidokit is deprecated upstream and expects native
|
|
186
197
|
BlazePose landmarks; the deterministic COCO-17 expansion is therefore an explicit accuracy
|
|
187
|
-
constraint, and
|
|
198
|
+
constraint, and retargeting still requires validation with real recordings before release.
|
|
188
199
|
|
|
189
200
|
## Frame sync pattern vertical slice
|
|
190
201
|
|
package/docs/index.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<h2>Install and configure</h2>
|
|
27
27
|
<p>Load the provider extensions you use first, then load the version-pinned bundle as an unsandboxed custom extension:</p>
|
|
28
|
-
<pre>https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-realtime-motion-capture@0.
|
|
28
|
+
<pre>https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-realtime-motion-capture@0.4.0/dist/turbowarp-realtime-motion-capture.js</pre>
|
|
29
29
|
<p>All feature groups are OFF by default. Set the complete startup configuration before loading the bundle:</p>
|
|
30
30
|
<pre><code>globalThis.__TWMP_FEATURE_FLAGS__ = {
|
|
31
31
|
qrCourierPairing: true,
|
|
@@ -66,9 +66,9 @@ globalThis.__TWMP_QR_CONFIG__ = {errorCorrectionLevel: "M"};</code></pre>
|
|
|
66
66
|
<p>The bundled, exact-pinned OpenCV.js 4.12 WebAssembly backend is the sole solver. It estimates intrinsic, distortion, and world-from-camera values and rejects insufficient samples, resolution changes, weak views, excessive reprojection RMS, unknown profile versions, and pairing credentials.</p>
|
|
67
67
|
|
|
68
68
|
<h2>PoseFrame3D avatar retargeting</h2>
|
|
69
|
-
<p>The independent <code>avatarRetargetV1</code> flag is OFF by default. The adapter requires TurboWarp-A-Frame 0.
|
|
70
|
-
<p>Corresponding external PoseFrame3D and PoseFrame2D v1 values can drive up to six
|
|
71
|
-
<p>The deterministic expansion uses reduced-visibility duplicates for unavailable hand, foot, and face landmarks and is less precise than native BlazePose input. Kalidokit is deprecated upstream, so
|
|
69
|
+
<p>The independent <code>avatarRetargetV1</code> flag is OFF by default. The adapter requires TurboWarp-A-Frame 0.4.0 scene capability v2, refuses any other version, and uses only its public template, transform, event, count, delete, and VRM operations.</p>
|
|
70
|
+
<p>Corresponding external PoseFrame3D and PoseFrame2D v1 values can drive up to six VRM avatars. The adapter matches person IDs, expands COCO-17 to BlazePose-33, and uses exact-pinned Kalidokit 1.1.5 as its only rotation solver before turning the VRM humanoid bones on the performer's own side.</p>
|
|
71
|
+
<p>The deterministic expansion uses reduced-visibility duplicates for unavailable hand, foot, and face landmarks and is less precise than native BlazePose input. Kalidokit is deprecated upstream, so retargeting requires validation with real recordings. This extension provides no custom solver fallback and does not align frames, query history, triangulate, or solve 3D data.</p>
|
|
72
72
|
<h2>Multi-camera 3D pose fusion</h2>
|
|
73
73
|
<p>The independent <code>poseFusion3D</code> flag is OFF by default. When enabled, blocks buffer PoseFrame2D JSON per camera in timestamp-ordered ring buffers, resample every camera at one past instant behind a configured delay, and triangulate the synchronized 2D sets into PoseFrame3D version 1 JSON.</p>
|
|
74
74
|
<p>Out-of-order frames inside the jitter window are reordered, while duplicates, late arrivals, and frames that no loaded calibration profile describes are counted as dropped. Occluded keypoints are filled from the visible side of their bracket, disagreeing views are resolved by the largest consensus set, and a keypoint without two confident views holds its last triangulated position with score zero.</p>
|