@doki-land/live2d 0.0.14 → 0.0.16
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 +189 -1
- package/dist/index.d.ts +55 -7
- package/dist/index.js +370 -203
- package/package.json +9 -7
- package/src/index.ts +3 -0
- package/src/stage/actor-model-slot.ts +63 -167
- package/src/stage/actor.ts +12 -7
- package/src/stage/model-asset-key.ts +19 -0
- package/src/stage/model-asset-registry.ts +337 -0
- package/src/stage/stage.ts +9 -1
package/dist/index.js
CHANGED
|
@@ -57,74 +57,6 @@ function valueFromNormalized(binding, normalized) {
|
|
|
57
57
|
return n >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * n : binding.defaultValue + (binding.defaultValue - binding.min) * n;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// src/stage/actor-model-slot.ts
|
|
61
|
-
import { modelSourceUrl } from "@doki-land/live2d-core";
|
|
62
|
-
import {
|
|
63
|
-
createUrlAssetResolver,
|
|
64
|
-
fetchModelJson,
|
|
65
|
-
normalizeModelSettings,
|
|
66
|
-
resolveModelSourceUrl
|
|
67
|
-
} from "@doki-land/live2d-loader";
|
|
68
|
-
import { selectModelBackend } from "@doki-land/live2d-renderer";
|
|
69
|
-
|
|
70
|
-
// src/load-textures.ts
|
|
71
|
-
function guessMime(path) {
|
|
72
|
-
const lower = path.toLowerCase();
|
|
73
|
-
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
74
|
-
if (lower.endsWith(".webp")) return "image/webp";
|
|
75
|
-
if (lower.endsWith(".gif")) return "image/gif";
|
|
76
|
-
return "image/png";
|
|
77
|
-
}
|
|
78
|
-
async function bytesToImageBitmap(bytes, path) {
|
|
79
|
-
if (typeof createImageBitmap !== "function") {
|
|
80
|
-
throw new Error(
|
|
81
|
-
"@doki-land/live2d: createImageBitmap is not available in this environment"
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
const blob = new Blob([new Uint8Array(bytes)], {
|
|
85
|
-
type: guessMime(path)
|
|
86
|
-
});
|
|
87
|
-
return createImageBitmap(blob);
|
|
88
|
-
}
|
|
89
|
-
async function loadTextureData(resolver, paths, options = {}) {
|
|
90
|
-
const out = [];
|
|
91
|
-
const total = paths.length;
|
|
92
|
-
for (let i = 0; i < paths.length; i++) {
|
|
93
|
-
const key = paths[i];
|
|
94
|
-
options.onProgress?.({
|
|
95
|
-
index: i,
|
|
96
|
-
total,
|
|
97
|
-
key,
|
|
98
|
-
bytesLoaded: 0,
|
|
99
|
-
bytesTotal: null
|
|
100
|
-
});
|
|
101
|
-
const bytes = await resolver.fetchBytes(key);
|
|
102
|
-
options.onProgress?.({
|
|
103
|
-
index: i,
|
|
104
|
-
total,
|
|
105
|
-
key,
|
|
106
|
-
bytesLoaded: bytes.byteLength,
|
|
107
|
-
bytesTotal: bytes.byteLength
|
|
108
|
-
});
|
|
109
|
-
const image = await bytesToImageBitmap(bytes, key);
|
|
110
|
-
out.push({
|
|
111
|
-
index: i,
|
|
112
|
-
image,
|
|
113
|
-
width: image.width,
|
|
114
|
-
height: image.height
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
function releaseTextureData(textures) {
|
|
120
|
-
for (const t of textures) {
|
|
121
|
-
const img = t.image;
|
|
122
|
-
if (typeof ImageBitmap !== "undefined" && img instanceof ImageBitmap) {
|
|
123
|
-
img.close();
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
60
|
// src/motion/evaluate-curve.ts
|
|
129
61
|
function evaluateMotion3(clip, timeSeconds) {
|
|
130
62
|
const t = clamp(timeSeconds, 0, clip.duration);
|
|
@@ -611,23 +543,18 @@ function optionalNum(v) {
|
|
|
611
543
|
}
|
|
612
544
|
|
|
613
545
|
// src/stage/actor-model-slot.ts
|
|
614
|
-
function lerp(a, b, t) {
|
|
615
|
-
return a + (b - a) * Math.min(1, Math.max(0, t));
|
|
616
|
-
}
|
|
617
546
|
var ActorModelSlot = class {
|
|
618
|
-
#
|
|
547
|
+
#assets;
|
|
619
548
|
#renderer;
|
|
620
549
|
#onProgress;
|
|
621
550
|
#motionPlayer;
|
|
622
|
-
#motionCache = /* @__PURE__ */ new Map();
|
|
623
551
|
#drawPass = null;
|
|
624
552
|
#model = null;
|
|
625
553
|
#backend = null;
|
|
626
|
-
#
|
|
627
|
-
#resolver = null;
|
|
554
|
+
#lease = null;
|
|
628
555
|
#loadGeneration = 0;
|
|
629
556
|
constructor(options) {
|
|
630
|
-
this.#
|
|
557
|
+
this.#assets = options.assets;
|
|
631
558
|
this.#renderer = options.renderer;
|
|
632
559
|
this.#onProgress = options.onProgress;
|
|
633
560
|
this.#motionPlayer = new MotionPlayer({
|
|
@@ -650,12 +577,9 @@ var ActorModelSlot = class {
|
|
|
650
577
|
#report(payload) {
|
|
651
578
|
this.#onProgress?.(payload);
|
|
652
579
|
}
|
|
653
|
-
#
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
this.#textures = [];
|
|
657
|
-
}
|
|
658
|
-
this.#drawPass?.setTextures([]);
|
|
580
|
+
#releaseLease() {
|
|
581
|
+
this.#lease?.release();
|
|
582
|
+
this.#lease = null;
|
|
659
583
|
}
|
|
660
584
|
#applyMotionSamples(samples) {
|
|
661
585
|
if (!this.#model || !this.#backend) return;
|
|
@@ -697,138 +621,59 @@ var ActorModelSlot = class {
|
|
|
697
621
|
detail: "prepare draw pass"
|
|
698
622
|
});
|
|
699
623
|
const drawPass = this.ensureDrawPass();
|
|
700
|
-
this.#
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
let json;
|
|
706
|
-
let baseUrl;
|
|
707
|
-
let settingsUrl;
|
|
708
|
-
if (typeof source === "object" && source.kind === "json") {
|
|
709
|
-
json = source.json;
|
|
710
|
-
baseUrl = source.baseUrl;
|
|
711
|
-
settingsUrl = source.baseUrl;
|
|
712
|
-
this.#report({
|
|
713
|
-
stage: "settings",
|
|
714
|
-
progress: 0.2,
|
|
715
|
-
detail: "inline settings"
|
|
716
|
-
});
|
|
717
|
-
} else {
|
|
718
|
-
const raw = typeof source === "string" ? source : source.kind === "npm" ? modelSourceUrl(source) : source.url;
|
|
719
|
-
const cdnBase = typeof source === "object" && source.kind === "npm" ? source.cdnBase : void 0;
|
|
720
|
-
const fetchUrl = resolveModelSourceUrl(raw, {
|
|
721
|
-
npmCdnBase: cdnBase
|
|
722
|
-
});
|
|
723
|
-
this.#report({
|
|
724
|
-
stage: "settings",
|
|
725
|
-
progress: 0.05,
|
|
726
|
-
detail: fetchUrl
|
|
727
|
-
});
|
|
728
|
-
json = await fetchModelJson(fetchUrl, (u) => {
|
|
729
|
-
const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
|
|
730
|
-
this.#report({
|
|
731
|
-
stage: "settings",
|
|
732
|
-
progress: lerp(0.05, 0.22, ratio),
|
|
733
|
-
detail: fetchUrl,
|
|
734
|
-
bytesLoaded: u.bytesLoaded,
|
|
735
|
-
bytesTotal: u.bytesTotal
|
|
736
|
-
});
|
|
737
|
-
});
|
|
738
|
-
baseUrl = fetchUrl;
|
|
739
|
-
settingsUrl = fetchUrl;
|
|
740
|
-
}
|
|
624
|
+
const lease = await this.#assets.acquire(
|
|
625
|
+
source,
|
|
626
|
+
resolver,
|
|
627
|
+
(p) => this.#report(p)
|
|
628
|
+
);
|
|
741
629
|
if (gen !== this.#loadGeneration) {
|
|
630
|
+
lease.release();
|
|
742
631
|
throw new Error("@doki-land/live2d: load cancelled");
|
|
743
632
|
}
|
|
744
|
-
|
|
633
|
+
return await this.#attachLease(lease, drawPass, gen);
|
|
634
|
+
}
|
|
635
|
+
async loadAsset(asset) {
|
|
636
|
+
const gen = ++this.#loadGeneration;
|
|
745
637
|
this.#report({
|
|
746
|
-
stage: "
|
|
747
|
-
progress: 0.
|
|
748
|
-
detail:
|
|
749
|
-
});
|
|
750
|
-
const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
|
|
751
|
-
onBytesProgress: (key, u) => {
|
|
752
|
-
const isMoc = key === settings.moc;
|
|
753
|
-
const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
|
|
754
|
-
if (isMoc) {
|
|
755
|
-
this.#report({
|
|
756
|
-
stage: "moc",
|
|
757
|
-
progress: lerp(0.25, 0.8, ratio),
|
|
758
|
-
detail: key,
|
|
759
|
-
bytesLoaded: u.bytesLoaded,
|
|
760
|
-
bytesTotal: u.bytesTotal
|
|
761
|
-
});
|
|
762
|
-
} else {
|
|
763
|
-
this.#report({
|
|
764
|
-
stage: "textures",
|
|
765
|
-
progress: lerp(0.8, 0.9, ratio),
|
|
766
|
-
detail: key,
|
|
767
|
-
bytesLoaded: u.bytesLoaded,
|
|
768
|
-
bytesTotal: u.bytesTotal
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
}
|
|
638
|
+
stage: "mounting",
|
|
639
|
+
progress: 0.01,
|
|
640
|
+
detail: "prepare draw pass"
|
|
772
641
|
});
|
|
773
|
-
|
|
774
|
-
this.#
|
|
775
|
-
this.#
|
|
776
|
-
|
|
642
|
+
const drawPass = this.ensureDrawPass();
|
|
643
|
+
const lease = this.#assets.acquireExisting(asset);
|
|
644
|
+
if (gen !== this.#loadGeneration) {
|
|
645
|
+
lease.release();
|
|
646
|
+
throw new Error("@doki-land/live2d: load cancelled");
|
|
647
|
+
}
|
|
777
648
|
this.#report({
|
|
778
649
|
stage: "decode",
|
|
779
650
|
progress: 0.85,
|
|
780
|
-
detail: `
|
|
781
|
-
});
|
|
782
|
-
const next = await backend.createModel(settings, {
|
|
783
|
-
renderer: this.#renderer,
|
|
784
|
-
resolver: assetResolver
|
|
651
|
+
detail: `reuse ${asset.key}`
|
|
785
652
|
});
|
|
653
|
+
return await this.#attachLease(lease, drawPass, gen);
|
|
654
|
+
}
|
|
655
|
+
async #attachLease(lease, drawPass, gen) {
|
|
656
|
+
this.#motionPlayer.clear();
|
|
657
|
+
this.#releaseLease();
|
|
658
|
+
const { model, backend } = await lease.createInstance(this.#renderer);
|
|
786
659
|
if (gen !== this.#loadGeneration) {
|
|
787
|
-
backend.destroyModel(
|
|
660
|
+
backend.destroyModel(model);
|
|
661
|
+
lease.release();
|
|
788
662
|
throw new Error("@doki-land/live2d: load cancelled");
|
|
789
663
|
}
|
|
790
|
-
this.#clearTextures();
|
|
791
|
-
if (settings.textures.length > 0) {
|
|
792
|
-
this.#report({
|
|
793
|
-
stage: "textures",
|
|
794
|
-
progress: 0.88,
|
|
795
|
-
detail: `${settings.textures.length} textures`
|
|
796
|
-
});
|
|
797
|
-
const textures = await loadTextureData(
|
|
798
|
-
assetResolver,
|
|
799
|
-
settings.textures,
|
|
800
|
-
{
|
|
801
|
-
onProgress: (u) => {
|
|
802
|
-
const ratio = u.total > 0 ? (u.index + 1) / u.total : 1;
|
|
803
|
-
this.#report({
|
|
804
|
-
stage: "textures",
|
|
805
|
-
progress: lerp(0.88, 0.96, ratio),
|
|
806
|
-
detail: u.key,
|
|
807
|
-
bytesLoaded: u.bytesLoaded,
|
|
808
|
-
bytesTotal: u.bytesTotal
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
);
|
|
813
|
-
if (gen !== this.#loadGeneration) {
|
|
814
|
-
releaseTextureData(textures);
|
|
815
|
-
backend.destroyModel(next);
|
|
816
|
-
throw new Error("@doki-land/live2d: load cancelled");
|
|
817
|
-
}
|
|
818
|
-
this.#textures = textures;
|
|
819
|
-
drawPass.setTextures(textures);
|
|
820
|
-
}
|
|
821
664
|
if (this.#model && this.#backend) {
|
|
822
665
|
this.#backend.destroyModel(this.#model);
|
|
823
666
|
}
|
|
824
|
-
|
|
667
|
+
drawPass.setTextures([...lease.textures]);
|
|
668
|
+
this.#lease = lease;
|
|
669
|
+
this.#model = model;
|
|
825
670
|
this.#backend = backend;
|
|
826
671
|
this.#report({
|
|
827
672
|
stage: "ready",
|
|
828
673
|
progress: 1,
|
|
829
|
-
detail:
|
|
674
|
+
detail: model.id
|
|
830
675
|
});
|
|
831
|
-
return
|
|
676
|
+
return model;
|
|
832
677
|
}
|
|
833
678
|
setParameter(id, value) {
|
|
834
679
|
if (!this.#model || !this.#backend?.setParameter) return;
|
|
@@ -842,15 +687,16 @@ var ActorModelSlot = class {
|
|
|
842
687
|
return this.#model?.settings.motionGroups ?? {};
|
|
843
688
|
}
|
|
844
689
|
async playMotion(group, index = 0, options = {}) {
|
|
845
|
-
if (!this.#model || !this.#
|
|
690
|
+
if (!this.#model || !this.#lease) return false;
|
|
846
691
|
const list = this.#model.settings.motionGroups[group];
|
|
847
692
|
const def = list?.[index];
|
|
848
693
|
if (!def) return false;
|
|
849
|
-
|
|
694
|
+
const cache = this.#lease.motionCache;
|
|
695
|
+
let clip = cache.get(def.file);
|
|
850
696
|
if (!clip) {
|
|
851
|
-
const json = await this.#resolver.fetchJson(def.file);
|
|
697
|
+
const json = await this.#lease.resolver.fetchJson(def.file);
|
|
852
698
|
clip = parseMotion3(json);
|
|
853
|
-
|
|
699
|
+
cache.set(def.file, clip);
|
|
854
700
|
}
|
|
855
701
|
const fadeInTime = options.fadeInTime ?? def.fadeInTime ?? clip.fadeInTime;
|
|
856
702
|
const fadeOutTime = options.fadeOutTime ?? def.fadeOutTime ?? clip.fadeOutTime;
|
|
@@ -904,14 +750,13 @@ var ActorModelSlot = class {
|
|
|
904
750
|
destroy() {
|
|
905
751
|
this.#loadGeneration += 1;
|
|
906
752
|
this.#motionPlayer.clear();
|
|
907
|
-
this.#motionCache.clear();
|
|
908
|
-
this.#resolver = null;
|
|
909
753
|
if (this.#model && this.#backend) {
|
|
910
754
|
this.#backend.destroyModel(this.#model);
|
|
911
755
|
}
|
|
912
756
|
this.#model = null;
|
|
913
757
|
this.#backend = null;
|
|
914
|
-
this.#
|
|
758
|
+
this.#releaseLease();
|
|
759
|
+
this.#drawPass?.setTextures([]);
|
|
915
760
|
this.#drawPass?.destroy();
|
|
916
761
|
this.#drawPass = null;
|
|
917
762
|
}
|
|
@@ -1042,7 +887,7 @@ var Live2dActorImpl = class {
|
|
|
1042
887
|
this.#layer = options?.layer ?? "characters";
|
|
1043
888
|
this.#order = options?.order ?? 0;
|
|
1044
889
|
this.#slot = new ActorModelSlot({
|
|
1045
|
-
|
|
890
|
+
assets: shared.assets,
|
|
1046
891
|
renderer: shared.renderer
|
|
1047
892
|
});
|
|
1048
893
|
}
|
|
@@ -1088,6 +933,12 @@ var Live2dActorImpl = class {
|
|
|
1088
933
|
}
|
|
1089
934
|
return await this.#slot.load(source, resolver);
|
|
1090
935
|
}
|
|
936
|
+
async loadAsset(asset) {
|
|
937
|
+
if (this.#destroyed) {
|
|
938
|
+
throw new Error("@doki-land/live2d: actor destroyed");
|
|
939
|
+
}
|
|
940
|
+
return await this.#slot.loadAsset(asset);
|
|
941
|
+
}
|
|
1091
942
|
setParameter(id, value) {
|
|
1092
943
|
this.#slot.setParameter(id, value);
|
|
1093
944
|
}
|
|
@@ -1330,9 +1181,320 @@ import {
|
|
|
1330
1181
|
createMoc3Backend,
|
|
1331
1182
|
createRenderer
|
|
1332
1183
|
} from "@doki-land/live2d-renderer";
|
|
1184
|
+
|
|
1185
|
+
// src/stage/model-asset-registry.ts
|
|
1186
|
+
import { modelSourceUrl as modelSourceUrl2 } from "@doki-land/live2d-core";
|
|
1187
|
+
import {
|
|
1188
|
+
createUrlAssetResolver,
|
|
1189
|
+
fetchModelJson,
|
|
1190
|
+
normalizeModelSettings,
|
|
1191
|
+
resolveModelSourceUrl
|
|
1192
|
+
} from "@doki-land/live2d-loader";
|
|
1193
|
+
import {
|
|
1194
|
+
compileSharedModelCompile,
|
|
1195
|
+
selectModelBackend
|
|
1196
|
+
} from "@doki-land/live2d-renderer";
|
|
1197
|
+
|
|
1198
|
+
// src/load-textures.ts
|
|
1199
|
+
function guessMime(path) {
|
|
1200
|
+
const lower = path.toLowerCase();
|
|
1201
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
1202
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
1203
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
1204
|
+
return "image/png";
|
|
1205
|
+
}
|
|
1206
|
+
async function bytesToImageBitmap(bytes, path) {
|
|
1207
|
+
if (typeof createImageBitmap !== "function") {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
"@doki-land/live2d: createImageBitmap is not available in this environment"
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
const blob = new Blob([new Uint8Array(bytes)], {
|
|
1213
|
+
type: guessMime(path)
|
|
1214
|
+
});
|
|
1215
|
+
return createImageBitmap(blob);
|
|
1216
|
+
}
|
|
1217
|
+
async function loadTextureData(resolver, paths, options = {}) {
|
|
1218
|
+
const out = [];
|
|
1219
|
+
const total = paths.length;
|
|
1220
|
+
for (let i = 0; i < paths.length; i++) {
|
|
1221
|
+
const key = paths[i];
|
|
1222
|
+
options.onProgress?.({
|
|
1223
|
+
index: i,
|
|
1224
|
+
total,
|
|
1225
|
+
key,
|
|
1226
|
+
bytesLoaded: 0,
|
|
1227
|
+
bytesTotal: null
|
|
1228
|
+
});
|
|
1229
|
+
const bytes = await resolver.fetchBytes(key);
|
|
1230
|
+
options.onProgress?.({
|
|
1231
|
+
index: i,
|
|
1232
|
+
total,
|
|
1233
|
+
key,
|
|
1234
|
+
bytesLoaded: bytes.byteLength,
|
|
1235
|
+
bytesTotal: bytes.byteLength
|
|
1236
|
+
});
|
|
1237
|
+
const image = await bytesToImageBitmap(bytes, key);
|
|
1238
|
+
out.push({
|
|
1239
|
+
index: i,
|
|
1240
|
+
image,
|
|
1241
|
+
width: image.width,
|
|
1242
|
+
height: image.height
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
return out;
|
|
1246
|
+
}
|
|
1247
|
+
function releaseTextureData(textures) {
|
|
1248
|
+
for (const t of textures) {
|
|
1249
|
+
const img = t.image;
|
|
1250
|
+
if (typeof ImageBitmap !== "undefined" && img instanceof ImageBitmap) {
|
|
1251
|
+
img.close();
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/stage/model-asset-key.ts
|
|
1257
|
+
import { modelSourceUrl } from "@doki-land/live2d-core";
|
|
1258
|
+
function stableJsonKey(value) {
|
|
1259
|
+
let h = 5381;
|
|
1260
|
+
const text = JSON.stringify(value);
|
|
1261
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
1262
|
+
h = (Math.imul(h, 33) ^ text.charCodeAt(i)) >>> 0;
|
|
1263
|
+
}
|
|
1264
|
+
return h.toString(36);
|
|
1265
|
+
}
|
|
1266
|
+
function resolveModelAssetKey(source) {
|
|
1267
|
+
if (typeof source === "object" && source.kind === "json") {
|
|
1268
|
+
return `json:${source.baseUrl}#${stableJsonKey(source.json)}`;
|
|
1269
|
+
}
|
|
1270
|
+
return modelSourceUrl(source);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/stage/model-asset-registry.ts
|
|
1274
|
+
function lerp(a, b, t) {
|
|
1275
|
+
return a + (b - a) * Math.min(1, Math.max(0, t));
|
|
1276
|
+
}
|
|
1277
|
+
var ModelAssetHandle = class {
|
|
1278
|
+
#entry;
|
|
1279
|
+
constructor(entry) {
|
|
1280
|
+
this.#entry = entry;
|
|
1281
|
+
}
|
|
1282
|
+
get key() {
|
|
1283
|
+
return this.#entry.key;
|
|
1284
|
+
}
|
|
1285
|
+
get settings() {
|
|
1286
|
+
return this.#entry.settings;
|
|
1287
|
+
}
|
|
1288
|
+
/** @internal */
|
|
1289
|
+
get entry() {
|
|
1290
|
+
return this.#entry;
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
var ModelAssetRegistry = class {
|
|
1294
|
+
#backends;
|
|
1295
|
+
#entries = /* @__PURE__ */ new Map();
|
|
1296
|
+
#inFlight = /* @__PURE__ */ new Map();
|
|
1297
|
+
constructor(options) {
|
|
1298
|
+
this.#backends = options.backends;
|
|
1299
|
+
}
|
|
1300
|
+
async load(source, resolver) {
|
|
1301
|
+
const entry = await this.#ensureEntry(source, resolver);
|
|
1302
|
+
return new ModelAssetHandle(entry);
|
|
1303
|
+
}
|
|
1304
|
+
async acquire(source, resolver, onProgress) {
|
|
1305
|
+
const entry = await this.#ensureEntry(source, resolver, onProgress);
|
|
1306
|
+
entry.refCount += 1;
|
|
1307
|
+
return this.#leaseFromEntry(entry);
|
|
1308
|
+
}
|
|
1309
|
+
acquireExisting(asset) {
|
|
1310
|
+
const handle = asset;
|
|
1311
|
+
const entry = handle.entry;
|
|
1312
|
+
if (!this.#entries.has(entry.key) || this.#entries.get(entry.key) !== entry) {
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
"@doki-land/live2d: ModelAsset does not belong to this stage"
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
entry.refCount += 1;
|
|
1318
|
+
return this.#leaseFromEntry(entry);
|
|
1319
|
+
}
|
|
1320
|
+
destroy() {
|
|
1321
|
+
this.#inFlight.clear();
|
|
1322
|
+
for (const entry of this.#entries.values()) {
|
|
1323
|
+
releaseTextureData(entry.textures);
|
|
1324
|
+
entry.motionCache.clear();
|
|
1325
|
+
}
|
|
1326
|
+
this.#entries.clear();
|
|
1327
|
+
}
|
|
1328
|
+
#leaseFromEntry(entry) {
|
|
1329
|
+
let released = false;
|
|
1330
|
+
return {
|
|
1331
|
+
asset: new ModelAssetHandle(entry),
|
|
1332
|
+
motionCache: entry.motionCache,
|
|
1333
|
+
resolver: entry.resolver,
|
|
1334
|
+
textures: entry.textures,
|
|
1335
|
+
createInstance: async (renderer) => {
|
|
1336
|
+
const model = await entry.backend.createModel(entry.settings, {
|
|
1337
|
+
renderer,
|
|
1338
|
+
resolver: entry.resolver,
|
|
1339
|
+
sharedCompile: entry.sharedCompile
|
|
1340
|
+
});
|
|
1341
|
+
return { model, backend: entry.backend };
|
|
1342
|
+
},
|
|
1343
|
+
release: () => {
|
|
1344
|
+
if (released) return;
|
|
1345
|
+
released = true;
|
|
1346
|
+
entry.refCount = Math.max(0, entry.refCount - 1);
|
|
1347
|
+
if (entry.refCount === 0) {
|
|
1348
|
+
releaseTextureData(entry.textures);
|
|
1349
|
+
entry.textures.length = 0;
|
|
1350
|
+
entry.motionCache.clear();
|
|
1351
|
+
this.#entries.delete(entry.key);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
async #ensureEntry(source, resolver, onProgress) {
|
|
1357
|
+
const key = resolveModelAssetKey(source);
|
|
1358
|
+
const existing = this.#entries.get(key);
|
|
1359
|
+
if (existing) return existing;
|
|
1360
|
+
let pending = this.#inFlight.get(key);
|
|
1361
|
+
if (!pending) {
|
|
1362
|
+
pending = this.#compileEntry(key, source, resolver, onProgress);
|
|
1363
|
+
this.#inFlight.set(key, pending);
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
const entry = await pending;
|
|
1367
|
+
this.#entries.set(key, entry);
|
|
1368
|
+
return entry;
|
|
1369
|
+
} finally {
|
|
1370
|
+
this.#inFlight.delete(key);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
async #compileEntry(key, source, resolver, onProgress) {
|
|
1374
|
+
const notify = (payload) => onProgress?.(payload);
|
|
1375
|
+
notify({
|
|
1376
|
+
stage: "resolve",
|
|
1377
|
+
progress: 0.02,
|
|
1378
|
+
detail: "resolve source"
|
|
1379
|
+
});
|
|
1380
|
+
let json;
|
|
1381
|
+
let baseUrl;
|
|
1382
|
+
let settingsUrl;
|
|
1383
|
+
if (typeof source === "object" && source.kind === "json") {
|
|
1384
|
+
json = source.json;
|
|
1385
|
+
baseUrl = source.baseUrl;
|
|
1386
|
+
settingsUrl = source.baseUrl;
|
|
1387
|
+
notify({
|
|
1388
|
+
stage: "settings",
|
|
1389
|
+
progress: 0.2,
|
|
1390
|
+
detail: "inline settings"
|
|
1391
|
+
});
|
|
1392
|
+
} else {
|
|
1393
|
+
const raw = typeof source === "string" ? source : source.kind === "npm" ? modelSourceUrl2(source) : source.url;
|
|
1394
|
+
const cdnBase = typeof source === "object" && source.kind === "npm" ? source.cdnBase : void 0;
|
|
1395
|
+
const fetchUrl = resolveModelSourceUrl(raw, {
|
|
1396
|
+
npmCdnBase: cdnBase
|
|
1397
|
+
});
|
|
1398
|
+
notify({
|
|
1399
|
+
stage: "settings",
|
|
1400
|
+
progress: 0.05,
|
|
1401
|
+
detail: fetchUrl
|
|
1402
|
+
});
|
|
1403
|
+
json = await fetchModelJson(fetchUrl, (u) => {
|
|
1404
|
+
const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
|
|
1405
|
+
notify({
|
|
1406
|
+
stage: "settings",
|
|
1407
|
+
progress: lerp(0.05, 0.22, ratio),
|
|
1408
|
+
detail: fetchUrl,
|
|
1409
|
+
bytesLoaded: u.bytesLoaded,
|
|
1410
|
+
bytesTotal: u.bytesTotal
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
baseUrl = fetchUrl;
|
|
1414
|
+
settingsUrl = fetchUrl;
|
|
1415
|
+
}
|
|
1416
|
+
const settings = normalizeModelSettings(json, settingsUrl);
|
|
1417
|
+
notify({
|
|
1418
|
+
stage: "moc",
|
|
1419
|
+
progress: 0.25,
|
|
1420
|
+
detail: settings.moc
|
|
1421
|
+
});
|
|
1422
|
+
const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
|
|
1423
|
+
onBytesProgress: (assetKey, u) => {
|
|
1424
|
+
const isMoc = assetKey === settings.moc;
|
|
1425
|
+
const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
|
|
1426
|
+
if (isMoc) {
|
|
1427
|
+
notify({
|
|
1428
|
+
stage: "moc",
|
|
1429
|
+
progress: lerp(0.25, 0.8, ratio),
|
|
1430
|
+
detail: assetKey,
|
|
1431
|
+
bytesLoaded: u.bytesLoaded,
|
|
1432
|
+
bytesTotal: u.bytesTotal
|
|
1433
|
+
});
|
|
1434
|
+
} else {
|
|
1435
|
+
notify({
|
|
1436
|
+
stage: "textures",
|
|
1437
|
+
progress: lerp(0.8, 0.9, ratio),
|
|
1438
|
+
detail: assetKey,
|
|
1439
|
+
bytesLoaded: u.bytesLoaded,
|
|
1440
|
+
bytesTotal: u.bytesTotal
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
const backend = selectModelBackend([...this.#backends], json);
|
|
1446
|
+
notify({
|
|
1447
|
+
stage: "decode",
|
|
1448
|
+
progress: 0.85,
|
|
1449
|
+
detail: `decode ${settings.format}`
|
|
1450
|
+
});
|
|
1451
|
+
const mocBytes = await assetResolver.fetchBytes(settings.moc);
|
|
1452
|
+
const sharedCompile = compileSharedModelCompile(settings, mocBytes);
|
|
1453
|
+
const textures = [];
|
|
1454
|
+
if (settings.textures.length > 0) {
|
|
1455
|
+
notify({
|
|
1456
|
+
stage: "textures",
|
|
1457
|
+
progress: 0.88,
|
|
1458
|
+
detail: `${settings.textures.length} textures`
|
|
1459
|
+
});
|
|
1460
|
+
textures.push(
|
|
1461
|
+
...await loadTextureData(assetResolver, settings.textures, {
|
|
1462
|
+
onProgress: (u) => {
|
|
1463
|
+
const ratio = u.total > 0 ? (u.index + 1) / u.total : 1;
|
|
1464
|
+
notify({
|
|
1465
|
+
stage: "textures",
|
|
1466
|
+
progress: lerp(0.88, 0.96, ratio),
|
|
1467
|
+
detail: u.key,
|
|
1468
|
+
bytesLoaded: u.bytesLoaded,
|
|
1469
|
+
bytesTotal: u.bytesTotal
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
})
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
notify({
|
|
1476
|
+
stage: "ready",
|
|
1477
|
+
progress: 1,
|
|
1478
|
+
detail: settings.name ?? key
|
|
1479
|
+
});
|
|
1480
|
+
return {
|
|
1481
|
+
key,
|
|
1482
|
+
settings,
|
|
1483
|
+
backend,
|
|
1484
|
+
resolver: assetResolver,
|
|
1485
|
+
sharedCompile,
|
|
1486
|
+
textures,
|
|
1487
|
+
motionCache: /* @__PURE__ */ new Map(),
|
|
1488
|
+
refCount: 0
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
// src/stage/stage.ts
|
|
1333
1494
|
var Live2dStageImpl = class {
|
|
1334
1495
|
#backends;
|
|
1335
1496
|
#renderer;
|
|
1497
|
+
#assets;
|
|
1336
1498
|
#updateMode;
|
|
1337
1499
|
#actors = /* @__PURE__ */ new Map();
|
|
1338
1500
|
#definedLayers = [
|
|
@@ -1367,8 +1529,12 @@ var Live2dStageImpl = class {
|
|
|
1367
1529
|
createMoc3Backend()
|
|
1368
1530
|
];
|
|
1369
1531
|
this.#renderer = options.renderer ?? createRenderer({ prefer: options.prefer });
|
|
1532
|
+
this.#assets = new ModelAssetRegistry({ backends: this.#backends });
|
|
1370
1533
|
this.#updateMode = options.updateMode ?? "auto";
|
|
1371
1534
|
}
|
|
1535
|
+
get assets() {
|
|
1536
|
+
return this.#assets;
|
|
1537
|
+
}
|
|
1372
1538
|
get actors() {
|
|
1373
1539
|
return [...this.#actors.values()];
|
|
1374
1540
|
}
|
|
@@ -1402,7 +1568,7 @@ var Live2dStageImpl = class {
|
|
|
1402
1568
|
const actor = new Live2dActorImpl(options, {
|
|
1403
1569
|
id,
|
|
1404
1570
|
creationIndex,
|
|
1405
|
-
|
|
1571
|
+
assets: this.#assets,
|
|
1406
1572
|
renderer: this.#renderer
|
|
1407
1573
|
});
|
|
1408
1574
|
this.#actors.set(id, actor);
|
|
@@ -1534,6 +1700,7 @@ var Live2dStageImpl = class {
|
|
|
1534
1700
|
actor.destroy();
|
|
1535
1701
|
}
|
|
1536
1702
|
this.#actors.clear();
|
|
1703
|
+
this.#assets.destroy();
|
|
1537
1704
|
this.#renderer.destroy();
|
|
1538
1705
|
this.#canvas = null;
|
|
1539
1706
|
this.#initPromise = null;
|