reze-engine 0.10.0 → 0.10.2
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 +126 -76
- package/dist/asset-reader.d.ts +16 -0
- package/dist/asset-reader.d.ts.map +1 -0
- package/dist/asset-reader.js +74 -0
- package/dist/engine.d.ts +9 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +60 -20
- package/dist/folder-upload.d.ts +24 -0
- package/dist/folder-upload.d.ts.map +1 -0
- package/dist/folder-upload.js +50 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/model.d.ts +9 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +46 -8
- package/dist/pmx-loader.d.ts +3 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +9 -2
- package/dist/vmd-writer.d.ts +5 -0
- package/dist/vmd-writer.d.ts.map +1 -0
- package/dist/vmd-writer.js +162 -0
- package/package.json +4 -3
- package/src/asset-reader.ts +79 -0
- package/src/engine.ts +84 -20
- package/src/folder-upload.ts +59 -0
- package/src/index.ts +3 -2
- package/src/model.ts +46 -9
- package/src/pmx-loader.ts +11 -2
- package/src/vmd-writer.ts +180 -0
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal-dependency WebGPU engine for real-time MMD/PMX rendering. Only external dependency is Ammo.js for physics.
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -14,80 +16,146 @@ npm install reze-engine
|
|
|
14
16
|
- VMD animation with IK solver and Bullet physics
|
|
15
17
|
- Orbit camera with bone-follow mode
|
|
16
18
|
- GPU picking (double-click/tap)
|
|
17
|
-
- Ground plane with PCF shadow mapping
|
|
19
|
+
- Ground plane with PCF shadow mapping
|
|
18
20
|
- Multi-model support
|
|
19
21
|
|
|
20
|
-
##
|
|
22
|
+
## Usage
|
|
21
23
|
|
|
22
24
|
```javascript
|
|
23
|
-
import { Engine, Vec3 } from "reze-engine"
|
|
25
|
+
import { Engine, Vec3 } from "reze-engine";
|
|
24
26
|
|
|
25
27
|
const engine = new Engine(canvas, {
|
|
26
28
|
ambientColor: new Vec3(0.88, 0.92, 0.99),
|
|
27
29
|
cameraDistance: 31.5, // MMD units (1 unit = 8 cm)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await
|
|
33
|
-
model.
|
|
34
|
-
model.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
engine.
|
|
38
|
-
engine.
|
|
30
|
+
cameraTarget: new Vec3(0, 11.5, 0),
|
|
31
|
+
});
|
|
32
|
+
await engine.init();
|
|
33
|
+
|
|
34
|
+
const model = await engine.loadModel("hero", "/models/hero/hero.pmx");
|
|
35
|
+
await model.loadVmd("idle", "/animations/idle.vmd");
|
|
36
|
+
model.show("idle");
|
|
37
|
+
model.play();
|
|
38
|
+
|
|
39
|
+
engine.setCameraFollow(model, "センター", new Vec3(0, 3.5, 0));
|
|
40
|
+
engine.addGround({ width: 160, height: 160 });
|
|
41
|
+
engine.runRenderLoop();
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
## API
|
|
42
45
|
|
|
46
|
+
One WebGPU **Engine** per page (singleton after `init()`). Load models via URL **or** from a user-selected folder (see [Local folder uploads](#local-folder-uploads-browser)).
|
|
47
|
+
|
|
43
48
|
### Engine
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
```javascript
|
|
51
|
+
engine.init()
|
|
52
|
+
engine.loadModel(name, path)
|
|
53
|
+
engine.loadModel(name, { files, pmxFile? }) // folder upload — see below
|
|
54
|
+
engine.getModel(name)
|
|
55
|
+
engine.getModelNames()
|
|
56
|
+
engine.removeModel(name)
|
|
57
|
+
|
|
58
|
+
engine.setMaterialVisible(name, material, visible)
|
|
59
|
+
engine.toggleMaterialVisible(name, material)
|
|
60
|
+
engine.isMaterialVisible(name, material)
|
|
61
|
+
|
|
62
|
+
engine.setIKEnabled(enabled)
|
|
63
|
+
engine.setPhysicsEnabled(enabled)
|
|
64
|
+
|
|
65
|
+
engine.setCameraFollow(model, bone?, offset?)
|
|
66
|
+
engine.setCameraFollow(null)
|
|
67
|
+
engine.setCameraTarget(vec3)
|
|
68
|
+
engine.setCameraDistance(d)
|
|
69
|
+
engine.setCameraAlpha(a)
|
|
70
|
+
engine.setCameraBeta(b)
|
|
71
|
+
|
|
72
|
+
engine.addGround(options?)
|
|
73
|
+
engine.runRenderLoop(callback?)
|
|
74
|
+
engine.stopRenderLoop()
|
|
75
|
+
engine.getStats()
|
|
76
|
+
engine.dispose()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Local folder uploads (browser)
|
|
80
|
+
|
|
81
|
+
Use a hidden `<input type="file" webkitdirectory multiple>` (or drag/drop) and pass the resulting `FileList` or `File[]` into the engine. Textures resolve relative to the chosen PMX file inside that tree.
|
|
82
|
+
|
|
83
|
+
**Important:** read `input.files` into a normal array **before** setting `input.value = ""`. The browser’s `FileList` is *live* — clearing the input empties it.
|
|
84
|
+
|
|
85
|
+
1. **`parsePmxFolderInput(fileList)`** — returns a tagged result (`empty` | `not_directory` | `no_pmx` | `single` | `multiple`). For `single`, you already have `files` and `pmxFile`. For `multiple`, show a picker (dropdown) of `pmxRelativePaths`, then resolve with **`pmxFileAtRelativePath(files, path)`**.
|
|
86
|
+
2. **`engine.loadModel(name, { files, pmxFile })`** — `pmxFile` selects which `.pmx` when the folder contains several.
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
import { Engine, parsePmxFolderInput, pmxFileAtRelativePath } from "reze-engine";
|
|
90
|
+
|
|
91
|
+
// In <input onChange>:
|
|
92
|
+
const picked = parsePmxFolderInput(e.target.files);
|
|
93
|
+
e.target.value = "";
|
|
94
|
+
|
|
95
|
+
if (picked.status === "single") {
|
|
96
|
+
const model = await engine.loadModel("myModel", {
|
|
97
|
+
files: picked.files,
|
|
98
|
+
pmxFile: picked.pmxFile,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (picked.status === "multiple") {
|
|
103
|
+
// Let the user choose `chosenPath` from picked.pmxRelativePaths, then:
|
|
104
|
+
const pmxFile = pmxFileAtRelativePath(picked.files, chosenPath);
|
|
105
|
+
const model = await engine.loadModel("myModel", { files: picked.files, pmxFile });
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
VMD and other assets still load by URL when the path starts with `/` or `http(s):`; relative paths are resolved against the PMX directory inside the upload.
|
|
68
110
|
|
|
69
111
|
### Model
|
|
70
112
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
```javascript
|
|
114
|
+
await model.loadVmd(name, url)
|
|
115
|
+
model.loadClip(name, clip)
|
|
116
|
+
model.show(name)
|
|
117
|
+
model.play(name)
|
|
118
|
+
model.play(name, { priority: 8 }) // higher number = higher priority (0 default/lowest)
|
|
119
|
+
model.play(name, { loop: true }) // repeat until stop/pause or another play
|
|
120
|
+
model.pause()
|
|
121
|
+
model.stop()
|
|
122
|
+
model.seek(time)
|
|
123
|
+
model.getAnimationProgress()
|
|
124
|
+
model.getClip(name)
|
|
125
|
+
model.exportVmd(name) // returns ArrayBuffer
|
|
126
|
+
|
|
127
|
+
model.rotateBones({ 首: quat, 頭: quat }, ms?)
|
|
128
|
+
model.moveBones({ センター: vec3 }, ms?)
|
|
129
|
+
model.setMorphWeight(name, weight, ms?)
|
|
130
|
+
model.resetAllBones()
|
|
131
|
+
model.resetAllMorphs()
|
|
132
|
+
model.getBoneWorldPosition(name)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Animation data
|
|
136
|
+
|
|
137
|
+
`AnimationClip` holds keyframes only: bone/morph tracks keyed by `frame`, and `frameCount` (last keyframe index). Time advances at fixed `FPS` (see package export `FPS`, default 30).
|
|
138
|
+
|
|
139
|
+
#### VMD Export
|
|
140
|
+
|
|
141
|
+
`model.exportVmd(name)` serialises a loaded clip back to the VMD binary format and returns an `ArrayBuffer`. Bone and morph names are Shift-JIS encoded for compatibility with standard MMD tools.
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
const buffer = model.exportVmd("idle");
|
|
145
|
+
const blob = new Blob([buffer], { type: "application/octet-stream" });
|
|
146
|
+
const link = document.createElement("a");
|
|
147
|
+
link.href = URL.createObjectURL(blob);
|
|
148
|
+
link.download = "idle.vmd";
|
|
149
|
+
link.click();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### Playback
|
|
153
|
+
|
|
154
|
+
Call `model.play(name, options?)` to start or switch motion. `loop: true` makes the playhead wrap at the end of the clip until you stop, pause, or call `play` with something else. `priority` chooses which request wins when several clips compete.
|
|
155
|
+
|
|
156
|
+
#### Progress
|
|
157
|
+
|
|
158
|
+
`getAnimationProgress()` reports `current` and `duration` in seconds, plus `playing`, `paused`, `looping`, and related fields.
|
|
91
159
|
|
|
92
160
|
### Engine Options
|
|
93
161
|
|
|
@@ -112,27 +180,9 @@ engine.runRenderLoop()
|
|
|
112
180
|
|
|
113
181
|
`constraintSolverKeywords` — joints whose name contains any keyword use the Bullet 2.75 constraint solver; all others keep the stable Ammo 2.82+ default. See [babylon-mmd: Fix Constraint Behavior](https://noname0310.github.io/babylon-mmd/docs/reference/runtime/apply-physics-to-mmd-models/#fix-constraint-behavior) for details.
|
|
114
182
|
|
|
115
|
-
### Ground Options
|
|
116
|
-
|
|
117
|
-
```javascript
|
|
118
|
-
engine.addGround({
|
|
119
|
-
width: 100, // ground plane width
|
|
120
|
-
height: 100, // ground plane depth
|
|
121
|
-
diffuseColor: Vec3, // base color (default: 0.8, 0.1, 1.0)
|
|
122
|
-
fadeStart: 5.0, // distance where edge fade begins
|
|
123
|
-
fadeEnd: 60.0, // distance where ground fully fades out
|
|
124
|
-
shadowMapSize: 4096, // shadow map resolution
|
|
125
|
-
shadowStrength: 1.0, // shadow darkness
|
|
126
|
-
gridSpacing: 5.0, // world-space distance between grid lines
|
|
127
|
-
gridLineWidth: 0.012, // thickness of grid lines
|
|
128
|
-
gridLineOpacity: 0.4, // grid line visibility (0–1)
|
|
129
|
-
gridLineColor: Vec3, // grid line color (default: 0.8, 0.8, 0.8)
|
|
130
|
-
noiseStrength: 0.08, // frosted/matte micro-texture intensity
|
|
131
|
-
})
|
|
132
|
-
```
|
|
133
|
-
|
|
134
183
|
## Projects Using This Engine
|
|
135
184
|
|
|
185
|
+
- **[Reze Studio](https://reze.studio)** - Web-native MMD animation editor
|
|
136
186
|
- **[MiKaPo](https://mikapo.vercel.app)** — Real-time motion capture for MMD
|
|
137
187
|
- **[Popo](https://popo.love)** — LLM-generated MMD poses
|
|
138
188
|
- **[MPL](https://mmd-mpl.vercel.app)** — Motion programming language for MMD
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Unified binary I/O for PMX/VMD/textures: HTTP(s) or user folder (File map). */
|
|
2
|
+
export type AssetReader = {
|
|
3
|
+
readBinary(logicalPath: string): Promise<ArrayBuffer>;
|
|
4
|
+
};
|
|
5
|
+
/** Normalize PMX-style paths: backslashes, trim, strip leading ./ */
|
|
6
|
+
export declare function normalizeAssetPath(p: string): string;
|
|
7
|
+
/** Join PMX directory prefix and texture-relative path (both may be ""). */
|
|
8
|
+
export declare function joinAssetPath(baseDir: string, relative: string): string;
|
|
9
|
+
/** Same rules as the original engine string split: supports absolute site paths like `/models/a/b.pmx`. */
|
|
10
|
+
export declare function deriveBasePathFromPmxPath(pmxPath: string): string;
|
|
11
|
+
export declare function createFetchAssetReader(): AssetReader;
|
|
12
|
+
/** Keys must be normalized paths relative to the selected folder root (see fileListToMap). */
|
|
13
|
+
export declare function createFileMapAssetReader(files: Map<string, File>): AssetReader;
|
|
14
|
+
export declare function fileListToMap(files: FileList | File[]): Map<string, File>;
|
|
15
|
+
export declare function findFirstPmxFileInList(files: FileList | File[]): File | null;
|
|
16
|
+
//# sourceMappingURL=asset-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-reader.d.ts","sourceRoot":"","sources":["../src/asset-reader.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAElF,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CACtD,CAAA;AAED,qEAAqE;AACrE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMvE;AAED,2GAA2G;AAC3G,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIjE;AAED,wBAAgB,sBAAsB,IAAI,WAAW,CAQpD;AAED,8FAA8F;AAC9F,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,WAAW,CAkB9E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAOzE;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,CAS5E"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Unified binary I/O for PMX/VMD/textures: HTTP(s) or user folder (File map). */
|
|
2
|
+
/** Normalize PMX-style paths: backslashes, trim, strip leading ./ */
|
|
3
|
+
export function normalizeAssetPath(p) {
|
|
4
|
+
let s = p.replace(/\\/g, "/").trim();
|
|
5
|
+
if (s.startsWith("./"))
|
|
6
|
+
s = s.slice(2);
|
|
7
|
+
return s;
|
|
8
|
+
}
|
|
9
|
+
/** Join PMX directory prefix and texture-relative path (both may be ""). */
|
|
10
|
+
export function joinAssetPath(baseDir, relative) {
|
|
11
|
+
const rel = normalizeAssetPath(relative);
|
|
12
|
+
if (!rel)
|
|
13
|
+
return normalizeAssetPath(baseDir);
|
|
14
|
+
const base = baseDir.endsWith("/") ? baseDir.slice(0, -1) : baseDir;
|
|
15
|
+
if (!base)
|
|
16
|
+
return rel;
|
|
17
|
+
return `${base}/${rel}`;
|
|
18
|
+
}
|
|
19
|
+
/** Same rules as the original engine string split: supports absolute site paths like `/models/a/b.pmx`. */
|
|
20
|
+
export function deriveBasePathFromPmxPath(pmxPath) {
|
|
21
|
+
const pathParts = pmxPath.replace(/\\/g, "/").split("/");
|
|
22
|
+
pathParts.pop();
|
|
23
|
+
return pathParts.join("/") + (pathParts.length > 0 ? "/" : "");
|
|
24
|
+
}
|
|
25
|
+
export function createFetchAssetReader() {
|
|
26
|
+
return {
|
|
27
|
+
async readBinary(logicalPath) {
|
|
28
|
+
const r = await fetch(logicalPath);
|
|
29
|
+
if (!r.ok)
|
|
30
|
+
throw new Error(`Failed to fetch ${logicalPath}: ${r.status} ${r.statusText}`);
|
|
31
|
+
return r.arrayBuffer();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Keys must be normalized paths relative to the selected folder root (see fileListToMap). */
|
|
36
|
+
export function createFileMapAssetReader(files) {
|
|
37
|
+
return {
|
|
38
|
+
async readBinary(logicalPath) {
|
|
39
|
+
const key = normalizeAssetPath(logicalPath);
|
|
40
|
+
let file = files.get(key);
|
|
41
|
+
if (!file) {
|
|
42
|
+
const lower = key.toLowerCase();
|
|
43
|
+
for (const [k, f] of files) {
|
|
44
|
+
if (k.toLowerCase() === lower) {
|
|
45
|
+
file = f;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!file)
|
|
51
|
+
throw new Error(`Missing file in folder: ${key}`);
|
|
52
|
+
return file.arrayBuffer();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function fileListToMap(files) {
|
|
57
|
+
const m = new Map();
|
|
58
|
+
for (const f of Array.from(files)) {
|
|
59
|
+
const rel = f.webkitRelativePath ?? f.name;
|
|
60
|
+
m.set(normalizeAssetPath(rel), f);
|
|
61
|
+
}
|
|
62
|
+
return m;
|
|
63
|
+
}
|
|
64
|
+
export function findFirstPmxFileInList(files) {
|
|
65
|
+
const list = Array.from(files).filter((f) => f.name.toLowerCase().endsWith(".pmx"));
|
|
66
|
+
if (list.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
list.sort((a, b) => {
|
|
69
|
+
const pa = a.webkitRelativePath ?? a.name;
|
|
70
|
+
const pb = b.webkitRelativePath ?? b.name;
|
|
71
|
+
return pa.localeCompare(pb);
|
|
72
|
+
});
|
|
73
|
+
return list[0] ?? null;
|
|
74
|
+
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Vec3 } from "./math";
|
|
2
2
|
import { Model } from "./model";
|
|
3
3
|
import { type PhysicsOptions } from "./physics";
|
|
4
|
+
import { type AssetReader } from "./asset-reader";
|
|
4
5
|
export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void;
|
|
6
|
+
/** Select a folder (webkitdirectory) and pass FileList or File[]; pmxFile picks which .pmx when several exist. */
|
|
7
|
+
export type LoadModelFromFilesOptions = {
|
|
8
|
+
files: FileList | File[];
|
|
9
|
+
pmxFile?: File;
|
|
10
|
+
};
|
|
5
11
|
export type EngineOptions = {
|
|
6
12
|
ambientColor?: Vec3;
|
|
7
13
|
directionalLightIntensity?: number;
|
|
@@ -154,7 +160,8 @@ export declare class Engine {
|
|
|
154
160
|
dispose(): void;
|
|
155
161
|
loadModel(path: string): Promise<Model>;
|
|
156
162
|
loadModel(name: string, path: string): Promise<Model>;
|
|
157
|
-
|
|
163
|
+
loadModel(name: string, options: LoadModelFromFilesOptions): Promise<Model>;
|
|
164
|
+
addModel(model: Model, pmxPath: string, name?: string, assetReader?: AssetReader): Promise<string>;
|
|
158
165
|
removeModel(name: string): void;
|
|
159
166
|
getModelNames(): string[];
|
|
160
167
|
getModel(name: string): Model | null;
|
|
@@ -178,7 +185,7 @@ export declare class Engine {
|
|
|
178
185
|
private createMaterialUniformBuffer;
|
|
179
186
|
private createUniformBuffer;
|
|
180
187
|
private shouldRenderDrawCall;
|
|
181
|
-
private
|
|
188
|
+
private createTextureFromLogicalPath;
|
|
182
189
|
private renderGround;
|
|
183
190
|
private handleCanvasDoubleClick;
|
|
184
191
|
private handleCanvasTouch;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,IAAI,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/B,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,IAAI,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/B,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AACxD,OAAO,EAQL,KAAK,WAAW,EACjB,MAAM,gBAAgB,CAAA;AAEvB,MAAM,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;AAEpH,kHAAkH;AAClH,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,CAAA;IACxB,OAAO,CAAC,EAAE,IAAI,CAAA;CACf,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B,oBAAoB,CAAC,EAAE,IAAI,CAAA;CAC5B,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;CAWlC,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;CAClB;AA8CD,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAsB;IAE7C,MAAM,CAAC,WAAW,IAAI,MAAM;IAO5B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,kBAAkB,CAAmB;IAC7C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,mBAAmB,CAAY;IACvC,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,kBAAkB,CAAY;IACtC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,oBAAoB,CAAoB;IAChD,OAAO,CAAC,2BAA2B,CAAqB;IACxD,OAAO,CAAC,eAAe,CAAoB;IAC3C,OAAO,CAAC,2BAA2B,CAAqB;IACxD,OAAO,CAAC,8BAA8B,CAAqB;IAC3D,OAAO,CAAC,8BAA8B,CAAqB;IAC3D,OAAO,CAAC,8BAA8B,CAAqB;IAC3D,OAAO,CAAC,iCAAiC,CAAqB;IAC9D,OAAO,CAAC,iBAAiB,CAAe;IACxC,OAAO,CAAC,wBAAwB,CAAe;IAC/C,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAI;IAC7C,OAAO,CAAC,oBAAoB,CAA0B;IAGtD,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,yBAAyB,CAAS;IAC1C,OAAO,CAAC,oBAAoB,CAAS;IAErC,OAAO,CAAC,iBAAiB,CAAS;IAGlC,OAAO,CAAC,kBAAkB,CAAC,CAAW;IACtC,OAAO,CAAC,iBAAiB,CAAC,CAAW;IACrC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,gBAAgB,CAAC,CAAY;IACrC,OAAO,CAAC,kBAAkB,CAAC,CAAgB;IAC3C,OAAO,CAAC,mBAAmB,CAAoB;IAC/C,OAAO,CAAC,mBAAmB,CAAY;IACvC,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,qBAAqB,CAAC,CAAc;IAC5C,OAAO,CAAC,uBAAuB,CAAa;IAC5C,OAAO,CAAC,0BAA0B,CAAC,CAAW;IAC9C,OAAO,CAAC,cAAc,CAAwB;IAE9C,OAAO,CAAC,SAAS,CAAC,CAAiB;IACnC,OAAO,CAAC,cAAc,CAAwD;IAC9E,OAAO,CAAC,oBAAoB,CAAoD;IAChF,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAM;IAEvC,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,2BAA2B,CAAqB;IACxD,OAAO,CAAC,8BAA8B,CAAqB;IAC3D,OAAO,CAAC,8BAA8B,CAAqB;IAC3D,OAAO,CAAC,qBAAqB,CAAe;IAC5C,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,kBAAkB,CAAY;IACtC,OAAO,CAAC,WAAW,CAAwC;IAE3D,OAAO,CAAC,cAAc,CAAmC;IACzD,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,mBAAmB,CAAI;IAG/B,OAAO,CAAC,SAAS,CAAO;IACxB,OAAO,CAAC,cAAc,CAAO;IAG7B,OAAO,CAAC,iBAAiB,CAAqB;IAC9C,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,kBAAkB,CAA0B;IAEpD,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,qBAAqB,CAAI;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,KAAK,CAGZ;IACD,OAAO,CAAC,gBAAgB,CAAsB;IAC9C,OAAO,CAAC,kBAAkB,CAA4B;gBAE1C,MAAM,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,aAAa;IAkBxD,IAAI;IA6BV,OAAO,CAAC,oBAAoB;IA+B5B,OAAO,CAAC,eAAe;IAumBvB,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,YAAY;IAwEpB,OAAO,CAAC,WAAW;IAanB,iFAAiF;IACjF,eAAe,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI;IAC9B,gGAAgG;IAChG,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI;IAoB3E,mIAAmI;IACnI,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI;IAY5E,iBAAiB,IAAI,MAAM;IAC3B,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAClC,cAAc,IAAI,MAAM;IACxB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAC/B,aAAa,IAAI,MAAM;IACvB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAG9B,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,QAAQ;IAmBhB,SAAS,CAAC,OAAO,CAAC,EAAE;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,YAAY,CAAC,EAAE,IAAI,CAAA;QACnB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,aAAa,CAAC,EAAE,IAAI,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;KACvB,GAAG,IAAI;IA4BR,OAAO,CAAC,iBAAiB;IAIzB,QAAQ,IAAI,WAAW;IAIvB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI;IAgBnC,cAAc;IAQd,OAAO;IAkBD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IACvC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IACrD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,KAAK,CAAC;IA4B3E,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAcxG,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAiB/B,aAAa,IAAI,MAAM,EAAE;IAIzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI;IAIpC,qBAAqB,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI;IAe9D,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAOnF,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAOpE,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO;IAKnE,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIpC,YAAY,IAAI,OAAO;IAIvB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIzC,iBAAiB,IAAI,OAAO;IAI5B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,kBAAkB;YAOZ,kBAAkB;IAqHhC,OAAO,CAAC,oBAAoB;IAwE5B,OAAO,CAAC,2BAA2B;IA2CnC,OAAO,CAAC,kBAAkB,CAAO;IACjC,OAAO,CAAC,mBAAmB;YAeb,yBAAyB;IAyFvC,OAAO,CAAC,2BAA2B;IAsBnC,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,oBAAoB;YAId,4BAA4B;IAkC1C,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,uBAAuB,CAI9B;IAED,OAAO,CAAC,iBAAiB,CA0BxB;IAED,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,cAAc;YA6CR,iBAAiB;IAuC/B,MAAM;IA+DN,OAAO,CAAC,kBAAkB;IAK1B,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,cAAc;IAetB,OAAO,CAAC,oBAAoB;IAa5B,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,WAAW;CAyBpB"}
|
package/dist/engine.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Camera } from "./camera";
|
|
|
2
2
|
import { Mat4, Vec3 } from "./math";
|
|
3
3
|
import { PmxLoader } from "./pmx-loader";
|
|
4
4
|
import { Physics } from "./physics";
|
|
5
|
+
import { createFetchAssetReader, createFileMapAssetReader, deriveBasePathFromPmxPath, fileListToMap, findFirstPmxFileInList, joinAssetPath, normalizeAssetPath, } from "./asset-reader";
|
|
5
6
|
export const DEFAULT_ENGINE_OPTIONS = {
|
|
6
7
|
ambientColor: new Vec3(0.88, 0.88, 0.88),
|
|
7
8
|
directionalLightIntensity: 0.24,
|
|
@@ -965,28 +966,55 @@ export class Engine {
|
|
|
965
966
|
this.resizeObserver = null;
|
|
966
967
|
}
|
|
967
968
|
}
|
|
968
|
-
async loadModel(nameOrPath,
|
|
969
|
-
|
|
970
|
-
|
|
969
|
+
async loadModel(nameOrPath, pathOrOptions) {
|
|
970
|
+
if (pathOrOptions !== undefined && typeof pathOrOptions === "object" && "files" in pathOrOptions) {
|
|
971
|
+
const name = nameOrPath;
|
|
972
|
+
const pmxFile = pathOrOptions.pmxFile ?? findFirstPmxFileInList(pathOrOptions.files);
|
|
973
|
+
if (!pmxFile)
|
|
974
|
+
throw new Error("No .pmx file found in the selected folder");
|
|
975
|
+
const map = fileListToMap(pathOrOptions.files);
|
|
976
|
+
const pmxKey = normalizeAssetPath(pmxFile.webkitRelativePath ?? pmxFile.name);
|
|
977
|
+
const reader = createFileMapAssetReader(map);
|
|
978
|
+
const model = await PmxLoader.loadFromReader(reader, pmxKey);
|
|
979
|
+
model.setName(name);
|
|
980
|
+
await this.addModel(model, pmxKey, name, reader);
|
|
981
|
+
return model;
|
|
982
|
+
}
|
|
983
|
+
const pmxPath = pathOrOptions === undefined ? nameOrPath : pathOrOptions;
|
|
984
|
+
const name = pathOrOptions === undefined ? "model_" + this._nextDefaultModelId++ : nameOrPath;
|
|
971
985
|
const model = await PmxLoader.load(pmxPath);
|
|
972
986
|
model.setName(name);
|
|
973
987
|
await this.addModel(model, pmxPath, name);
|
|
974
988
|
return model;
|
|
975
989
|
}
|
|
976
|
-
async addModel(model, pmxPath, name) {
|
|
990
|
+
async addModel(model, pmxPath, name, assetReader) {
|
|
977
991
|
const requested = name ?? model.name;
|
|
978
992
|
let key = requested;
|
|
979
993
|
let n = 1;
|
|
980
994
|
while (this.modelInstances.has(key)) {
|
|
981
995
|
key = `${requested}_${n++}`;
|
|
982
996
|
}
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
await this.setupModelInstance(key, model, basePath);
|
|
997
|
+
const reader = assetReader ?? createFetchAssetReader();
|
|
998
|
+
const basePath = deriveBasePathFromPmxPath(pmxPath);
|
|
999
|
+
model.setAssetContext(reader, basePath);
|
|
1000
|
+
await this.setupModelInstance(key, model, basePath, reader);
|
|
987
1001
|
return key;
|
|
988
1002
|
}
|
|
989
1003
|
removeModel(name) {
|
|
1004
|
+
const inst = this.modelInstances.get(name);
|
|
1005
|
+
if (!inst)
|
|
1006
|
+
return;
|
|
1007
|
+
inst.model.stopAnimation();
|
|
1008
|
+
for (const path of inst.textureCacheKeys) {
|
|
1009
|
+
const tex = this.textureCache.get(path);
|
|
1010
|
+
if (tex) {
|
|
1011
|
+
tex.destroy();
|
|
1012
|
+
this.textureCache.delete(path);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
for (const buf of inst.gpuBuffers) {
|
|
1016
|
+
buf.destroy();
|
|
1017
|
+
}
|
|
990
1018
|
this.modelInstances.delete(name);
|
|
991
1019
|
}
|
|
992
1020
|
getModelNames() {
|
|
@@ -1068,7 +1096,7 @@ export class Engine {
|
|
|
1068
1096
|
this.device.queue.writeBuffer(inst.vertexBuffer, 0, vertices);
|
|
1069
1097
|
inst.vertexBufferNeedsUpdate = false;
|
|
1070
1098
|
}
|
|
1071
|
-
async setupModelInstance(name, model, basePath) {
|
|
1099
|
+
async setupModelInstance(name, model, basePath, assetReader) {
|
|
1072
1100
|
const vertices = model.getVertices();
|
|
1073
1101
|
const skinning = model.getSkinning();
|
|
1074
1102
|
const skeleton = model.getSkeleton();
|
|
@@ -1130,10 +1158,20 @@ export class Engine {
|
|
|
1130
1158
|
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
1131
1159
|
],
|
|
1132
1160
|
});
|
|
1161
|
+
const gpuBuffers = [
|
|
1162
|
+
vertexBuffer,
|
|
1163
|
+
indexBuffer,
|
|
1164
|
+
jointsBuffer,
|
|
1165
|
+
weightsBuffer,
|
|
1166
|
+
skinMatrixBuffer,
|
|
1167
|
+
];
|
|
1133
1168
|
const inst = {
|
|
1134
1169
|
name,
|
|
1135
1170
|
model,
|
|
1136
1171
|
basePath,
|
|
1172
|
+
assetReader,
|
|
1173
|
+
gpuBuffers,
|
|
1174
|
+
textureCacheKeys: [],
|
|
1137
1175
|
vertexBuffer,
|
|
1138
1176
|
indexBuffer,
|
|
1139
1177
|
jointsBuffer,
|
|
@@ -1285,8 +1323,8 @@ export class Engine {
|
|
|
1285
1323
|
const loadTextureByIndex = async (texIndex) => {
|
|
1286
1324
|
if (texIndex < 0 || texIndex >= textures.length)
|
|
1287
1325
|
return null;
|
|
1288
|
-
const
|
|
1289
|
-
return this.
|
|
1326
|
+
const logicalPath = joinAssetPath(inst.basePath, normalizeAssetPath(textures[texIndex].path));
|
|
1327
|
+
return this.createTextureFromLogicalPath(inst, logicalPath);
|
|
1290
1328
|
};
|
|
1291
1329
|
let currentIndexOffset = 0;
|
|
1292
1330
|
let materialId = 0;
|
|
@@ -1301,6 +1339,7 @@ export class Engine {
|
|
|
1301
1339
|
const materialAlpha = mat.diffuse[3];
|
|
1302
1340
|
const isTransparent = materialAlpha < 1.0 - 0.001;
|
|
1303
1341
|
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, materialAlpha, [mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]], mat.ambient, mat.specular, mat.shininess);
|
|
1342
|
+
inst.gpuBuffers.push(materialUniformBuffer);
|
|
1304
1343
|
const textureView = diffuseTexture.createView();
|
|
1305
1344
|
const bindGroup = this.device.createBindGroup({
|
|
1306
1345
|
label: `${prefix}material: ${mat.name}`,
|
|
@@ -1318,6 +1357,7 @@ export class Engine {
|
|
|
1318
1357
|
mat.edgeSize, 0, 0, 0,
|
|
1319
1358
|
]);
|
|
1320
1359
|
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData);
|
|
1360
|
+
inst.gpuBuffers.push(outlineUniformBuffer);
|
|
1321
1361
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1322
1362
|
label: `${prefix}outline: ${mat.name}`,
|
|
1323
1363
|
layout: this.outlinePerMaterialBindGroupLayout,
|
|
@@ -1331,6 +1371,7 @@ export class Engine {
|
|
|
1331
1371
|
if (this.onRaycast) {
|
|
1332
1372
|
const pickIdData = new Float32Array([modelId, materialId, 0, 0]);
|
|
1333
1373
|
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData);
|
|
1374
|
+
inst.gpuBuffers.push(pickIdBuffer);
|
|
1334
1375
|
const pickBindGroup = this.device.createBindGroup({
|
|
1335
1376
|
label: `${prefix}pick: ${mat.name}`,
|
|
1336
1377
|
layout: this.pickPerMaterialBindGroupLayout,
|
|
@@ -1371,22 +1412,20 @@ export class Engine {
|
|
|
1371
1412
|
shouldRenderDrawCall(inst, drawCall) {
|
|
1372
1413
|
return !inst.hiddenMaterials.has(drawCall.materialName);
|
|
1373
1414
|
}
|
|
1374
|
-
async
|
|
1375
|
-
const
|
|
1415
|
+
async createTextureFromLogicalPath(inst, logicalPath) {
|
|
1416
|
+
const cacheKey = logicalPath;
|
|
1417
|
+
const cached = this.textureCache.get(cacheKey);
|
|
1376
1418
|
if (cached) {
|
|
1377
1419
|
return cached;
|
|
1378
1420
|
}
|
|
1379
1421
|
try {
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1383
|
-
}
|
|
1384
|
-
const imageBitmap = await createImageBitmap(await response.blob(), {
|
|
1422
|
+
const buffer = await inst.assetReader.readBinary(logicalPath);
|
|
1423
|
+
const imageBitmap = await createImageBitmap(new Blob([buffer]), {
|
|
1385
1424
|
premultiplyAlpha: "none",
|
|
1386
1425
|
colorSpaceConversion: "none",
|
|
1387
1426
|
});
|
|
1388
1427
|
const texture = this.device.createTexture({
|
|
1389
|
-
label: `texture: ${
|
|
1428
|
+
label: `texture: ${cacheKey}`,
|
|
1390
1429
|
size: [imageBitmap.width, imageBitmap.height],
|
|
1391
1430
|
format: "rgba8unorm",
|
|
1392
1431
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
@@ -1395,7 +1434,8 @@ export class Engine {
|
|
|
1395
1434
|
imageBitmap.width,
|
|
1396
1435
|
imageBitmap.height,
|
|
1397
1436
|
]);
|
|
1398
|
-
this.textureCache.set(
|
|
1437
|
+
this.textureCache.set(cacheKey, texture);
|
|
1438
|
+
inst.textureCacheKeys.push(cacheKey);
|
|
1399
1439
|
return texture;
|
|
1400
1440
|
}
|
|
1401
1441
|
catch {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** After choosing a path from `multiple`, get the `File` for `loadModel(..., { files, pmxFile })`. */
|
|
2
|
+
export declare function pmxFileAtRelativePath(files: File[], relativePath: string): File | undefined;
|
|
3
|
+
/** Result of reading a folder input — switch on `status` in your UI. */
|
|
4
|
+
export type PmxFolderInputResult = {
|
|
5
|
+
status: "empty";
|
|
6
|
+
} | {
|
|
7
|
+
status: "not_directory";
|
|
8
|
+
} | {
|
|
9
|
+
status: "no_pmx";
|
|
10
|
+
} | {
|
|
11
|
+
status: "single";
|
|
12
|
+
files: File[];
|
|
13
|
+
pmxFile: File;
|
|
14
|
+
} | {
|
|
15
|
+
status: "multiple";
|
|
16
|
+
files: File[];
|
|
17
|
+
pmxRelativePaths: string[];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* One call from `onChange`: snapshots files, validates folder pick, resolves a single PMX or asks you to pick among several.
|
|
21
|
+
* Reset the input after: `e.target.value = ""`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parsePmxFolderInput(fileList: FileList | null | undefined): PmxFolderInputResult;
|
|
24
|
+
//# sourceMappingURL=folder-upload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-upload.d.ts","sourceRoot":"","sources":["../src/folder-upload.ts"],"names":[],"mappings":"AAyBA,sGAAsG;AACtG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAO3F;AAED,wEAAwE;AACxE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACnB;IAAE,MAAM,EAAE,eAAe,CAAA;CAAE,GAC3B;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,GAClD;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAErE;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,GAAG,oBAAoB,CAW/F"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { normalizeAssetPath } from "./asset-reader";
|
|
2
|
+
/**
|
|
3
|
+
* Call on `<input type="file" webkitdirectory>` `change` **before** `input.value = ""`.
|
|
4
|
+
* `FileList` is live — clearing the input empties it; this copies to a stable `File[]`.
|
|
5
|
+
*/
|
|
6
|
+
function prepareLocalFolderFiles(fileList) {
|
|
7
|
+
const files = fileList?.length ? Array.from(fileList) : [];
|
|
8
|
+
const pmxRelativePaths = [];
|
|
9
|
+
for (const f of files) {
|
|
10
|
+
const wr = f.webkitRelativePath;
|
|
11
|
+
if (!wr || !wr.toLowerCase().endsWith(".pmx"))
|
|
12
|
+
continue;
|
|
13
|
+
pmxRelativePaths.push(normalizeAssetPath(wr));
|
|
14
|
+
}
|
|
15
|
+
pmxRelativePaths.sort((a, b) => a.localeCompare(b));
|
|
16
|
+
return { files, pmxRelativePaths };
|
|
17
|
+
}
|
|
18
|
+
function isDirectoryUpload(files) {
|
|
19
|
+
return files.length > 0 && files.every((f) => !!f.webkitRelativePath);
|
|
20
|
+
}
|
|
21
|
+
/** After choosing a path from `multiple`, get the `File` for `loadModel(..., { files, pmxFile })`. */
|
|
22
|
+
export function pmxFileAtRelativePath(files, relativePath) {
|
|
23
|
+
const norm = normalizeAssetPath(relativePath);
|
|
24
|
+
for (const f of files) {
|
|
25
|
+
const wr = f.webkitRelativePath;
|
|
26
|
+
if (wr && normalizeAssetPath(wr) === norm)
|
|
27
|
+
return f;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* One call from `onChange`: snapshots files, validates folder pick, resolves a single PMX or asks you to pick among several.
|
|
33
|
+
* Reset the input after: `e.target.value = ""`.
|
|
34
|
+
*/
|
|
35
|
+
export function parsePmxFolderInput(fileList) {
|
|
36
|
+
const { files, pmxRelativePaths } = prepareLocalFolderFiles(fileList);
|
|
37
|
+
if (files.length === 0)
|
|
38
|
+
return { status: "empty" };
|
|
39
|
+
if (!isDirectoryUpload(files))
|
|
40
|
+
return { status: "not_directory" };
|
|
41
|
+
if (pmxRelativePaths.length === 0)
|
|
42
|
+
return { status: "no_pmx" };
|
|
43
|
+
if (pmxRelativePaths.length === 1) {
|
|
44
|
+
const pmxFile = pmxFileAtRelativePath(files, pmxRelativePaths[0]);
|
|
45
|
+
if (!pmxFile)
|
|
46
|
+
return { status: "no_pmx" };
|
|
47
|
+
return { status: "single", files, pmxFile };
|
|
48
|
+
}
|
|
49
|
+
return { status: "multiple", files, pmxRelativePaths };
|
|
50
|
+
}
|