bucciafico-lib 1.0.7 → 1.0.8
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/package.json +1 -1
- package/src/core/SkinViewer.js +154 -9
- package/src/managers/EventManager.js +74 -0
- package/src/managers/PostProcessingManager.js +24 -0
- package/src/objects/SceneSetup.js +49 -8
- package/src/objects/SkinModel.js +205 -12
- package/src/plugins/EditorPlugin.js +45 -14
- package/src/plugins/EffectsPlugin.js +45 -20
- package/src/plugins/IOPlugin.js +46 -17
- package/src/plugins/ItemsPlugin.js +121 -2
- package/src/utils/SkinUtils.js +17 -17
- package/src/utils/ThreeUtils.js +27 -0
- package/src/utils/Voxelizer.js +139 -33
package/package.json
CHANGED
package/src/core/SkinViewer.js
CHANGED
|
@@ -3,6 +3,8 @@ import { CameraManager } from '../managers/CameraManager.js';
|
|
|
3
3
|
import { SceneSetup } from '../objects/SceneSetup.js';
|
|
4
4
|
import { SkinModel } from '../objects/SkinModel.js';
|
|
5
5
|
import { detectSlimSkin } from '../utils/SkinUtils.js';
|
|
6
|
+
import {disposeObjectTree} from "../utils/ThreeUtils.js";
|
|
7
|
+
import {EventManager} from "../managers/EventManager.js";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Core 3D Viewer class.
|
|
@@ -30,6 +32,9 @@ export class SkinViewer {
|
|
|
30
32
|
...config
|
|
31
33
|
};
|
|
32
34
|
|
|
35
|
+
this.skinData = null;
|
|
36
|
+
this.capeData = null;
|
|
37
|
+
|
|
33
38
|
/** @type {Map<string, Object>} Registered plugins. */
|
|
34
39
|
this.plugins = new Map();
|
|
35
40
|
|
|
@@ -39,6 +44,12 @@ export class SkinViewer {
|
|
|
39
44
|
this.isVisible = true;
|
|
40
45
|
this.needsRender = true;
|
|
41
46
|
|
|
47
|
+
this.events = new EventManager();
|
|
48
|
+
|
|
49
|
+
this.on = this.events.on.bind(this.events);
|
|
50
|
+
this.off = this.events.off.bind(this.events);
|
|
51
|
+
this.emit = this.events.emit.bind(this.events);
|
|
52
|
+
|
|
42
53
|
// --- 1. RENDERER SETUP ---
|
|
43
54
|
this.renderer = new THREE.WebGLRenderer({
|
|
44
55
|
antialias: true,
|
|
@@ -88,6 +99,7 @@ export class SkinViewer {
|
|
|
88
99
|
|
|
89
100
|
// --- 3. START LOOP ---
|
|
90
101
|
this.animate = this.animate.bind(this);
|
|
102
|
+
this.emit('viewer:ready', this);
|
|
91
103
|
this.animate();
|
|
92
104
|
}
|
|
93
105
|
|
|
@@ -130,8 +142,13 @@ export class SkinViewer {
|
|
|
130
142
|
* @returns {Promise<boolean>} isSlim
|
|
131
143
|
*/
|
|
132
144
|
loadSkin(imageUrl) {
|
|
145
|
+
this.emit('skin:loading', imageUrl);
|
|
146
|
+
|
|
133
147
|
return new Promise((resolve, reject) => {
|
|
134
|
-
new THREE.TextureLoader()
|
|
148
|
+
const loader = new THREE.TextureLoader();
|
|
149
|
+
loader.setCrossOrigin('anonymous');
|
|
150
|
+
|
|
151
|
+
loader.load(imageUrl, (texture) => {
|
|
135
152
|
texture.magFilter = THREE.NearestFilter;
|
|
136
153
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
137
154
|
|
|
@@ -145,9 +162,20 @@ export class SkinViewer {
|
|
|
145
162
|
this.skinModel.setPose(currentPose);
|
|
146
163
|
this.skinData = { type: 'url', value: imageUrl };
|
|
147
164
|
|
|
165
|
+
const fxPlugin = this.getPlugin('EffectsPlugin');
|
|
166
|
+
if (fxPlugin) {
|
|
167
|
+
fxPlugin.forceUpdate();
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
this.requestRender();
|
|
171
|
+
|
|
172
|
+
this.emit('skin:loaded', { isSlim, texture });
|
|
173
|
+
|
|
149
174
|
resolve(isSlim);
|
|
150
|
-
}, undefined,
|
|
175
|
+
}, undefined, (err) => {
|
|
176
|
+
this.emit('skin:error', err);
|
|
177
|
+
reject(err);
|
|
178
|
+
});
|
|
151
179
|
});
|
|
152
180
|
}
|
|
153
181
|
|
|
@@ -161,6 +189,96 @@ export class SkinViewer {
|
|
|
161
189
|
});
|
|
162
190
|
}
|
|
163
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Loads a cape from URL.
|
|
194
|
+
* @param {string} imageUrl
|
|
195
|
+
*/
|
|
196
|
+
loadCape(imageUrl) {
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
const loader = new THREE.TextureLoader();
|
|
199
|
+
loader.setCrossOrigin('anonymous');
|
|
200
|
+
|
|
201
|
+
loader.load(
|
|
202
|
+
imageUrl,
|
|
203
|
+
(texture) => {
|
|
204
|
+
texture.magFilter = THREE.NearestFilter;
|
|
205
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
206
|
+
|
|
207
|
+
texture.needsUpdate = true;
|
|
208
|
+
|
|
209
|
+
this.skinModel.setCape(texture);
|
|
210
|
+
|
|
211
|
+
const fxPlugin = this.getPlugin('EffectsPlugin');
|
|
212
|
+
if (fxPlugin) {
|
|
213
|
+
fxPlugin.forceUpdate();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.capeData = { type: 'url', value: imageUrl };
|
|
217
|
+
|
|
218
|
+
this.requestRender();
|
|
219
|
+
this.emit('cape:loaded', imageUrl);
|
|
220
|
+
resolve();
|
|
221
|
+
},
|
|
222
|
+
undefined,
|
|
223
|
+
(err) => {
|
|
224
|
+
console.error("Error loading cape texture:", err);
|
|
225
|
+
reject(err);
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Loads a cape by username using capes.dev API.
|
|
234
|
+
* Supports Official, Optifine, LabyMod, etc.
|
|
235
|
+
* @param {string} username
|
|
236
|
+
*/
|
|
237
|
+
async loadCapeByUsername(username) {
|
|
238
|
+
this.emit('cape:loading', username);
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const response = await fetch(`https://api.capes.dev/load/${username}`);
|
|
242
|
+
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
throw new Error(`User not found in capes.dev (Status: ${response.status})`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const data = await response.json();
|
|
248
|
+
let capeUrl = null;
|
|
249
|
+
|
|
250
|
+
if (data.minecraft?.exists && data.minecraft?.imageUrl) {
|
|
251
|
+
capeUrl = data.minecraft.imageUrl;
|
|
252
|
+
} else if (data.optifine?.exists && data.optifine?.imageUrl) {
|
|
253
|
+
capeUrl = data.optifine.imageUrl;
|
|
254
|
+
} else if (data.labymod?.exists && data.labymod?.imageUrl) {
|
|
255
|
+
capeUrl = data.labymod.imageUrl;
|
|
256
|
+
} else if (data.tlauncher?.exists && data.tlauncher?.imageUrl) {
|
|
257
|
+
capeUrl = data.tlauncher.imageUrl;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (capeUrl) {
|
|
261
|
+
await this.loadCape(capeUrl);
|
|
262
|
+
this.capeData = { type: 'username', value: username };
|
|
263
|
+
return true;
|
|
264
|
+
} else {
|
|
265
|
+
this.resetCape();
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
} catch (e) {
|
|
270
|
+
this.emit('cape:error', e);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
resetCape() {
|
|
276
|
+
this.skinModel.setCape(null);
|
|
277
|
+
this.capeData = null;
|
|
278
|
+
this.requestRender();
|
|
279
|
+
this.emit('cape:removed');
|
|
280
|
+
}
|
|
281
|
+
|
|
164
282
|
setPose(poseData) {
|
|
165
283
|
// Record history if Editor is present
|
|
166
284
|
const editor = this.getPlugin('EditorPlugin');
|
|
@@ -170,6 +288,15 @@ export class SkinViewer {
|
|
|
170
288
|
this.requestRender();
|
|
171
289
|
}
|
|
172
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Updates lighting intensity.
|
|
293
|
+
* @param {Object} config - { global, main, fill }
|
|
294
|
+
*/
|
|
295
|
+
setEnvironment(config) {
|
|
296
|
+
this.sceneSetup.setLightConfig(config);
|
|
297
|
+
this.requestRender();
|
|
298
|
+
}
|
|
299
|
+
|
|
173
300
|
/**
|
|
174
301
|
* Handles window resize. Should be called by the implementation layer.
|
|
175
302
|
*/
|
|
@@ -215,19 +342,37 @@ export class SkinViewer {
|
|
|
215
342
|
}
|
|
216
343
|
|
|
217
344
|
dispose() {
|
|
345
|
+
this.emit('viewer:dispose');
|
|
218
346
|
this.isDisposed = true;
|
|
219
347
|
this.observer.disconnect();
|
|
220
348
|
|
|
221
|
-
if (this.container && this.renderer.domElement) {
|
|
222
|
-
this.container.removeChild(this.renderer.domElement);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
this.renderer.dispose();
|
|
226
|
-
|
|
227
|
-
// Dispose all plugins
|
|
228
349
|
this.plugins.forEach(p => {
|
|
229
350
|
if (p.dispose) p.dispose();
|
|
230
351
|
});
|
|
231
352
|
this.plugins.clear();
|
|
353
|
+
|
|
354
|
+
if (this.skinModel) {
|
|
355
|
+
this.skinModel.dispose();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
disposeObjectTree(this.scene);
|
|
359
|
+
disposeObjectTree(this.overlayScene);
|
|
360
|
+
|
|
361
|
+
if (this.cameraManager) {
|
|
362
|
+
this.cameraManager.controls.dispose();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (this.renderer) {
|
|
366
|
+
this.renderer.dispose();
|
|
367
|
+
this.renderer.forceContextLoss();
|
|
368
|
+
|
|
369
|
+
if (this.container && this.renderer.domElement) {
|
|
370
|
+
this.container.removeChild(this.renderer.domElement);
|
|
371
|
+
}
|
|
372
|
+
this.renderer.domElement = null;
|
|
373
|
+
this.renderer = null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.events.dispose();
|
|
232
377
|
}
|
|
233
378
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Event Emitter implementation.
|
|
3
|
+
* Handles Pub/Sub architecture to decouple Core from Plugins and UI.
|
|
4
|
+
*/
|
|
5
|
+
export class EventManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
/** @type {Map<string, Set<Function>>} */
|
|
8
|
+
this.listeners = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to an event.
|
|
13
|
+
* @param {string} event - Event name (e.g. 'skin:loaded').
|
|
14
|
+
* @param {Function} callback - Function to execute.
|
|
15
|
+
* @returns {Function} Unsubscribe function for convenience.
|
|
16
|
+
*/
|
|
17
|
+
on(event, callback) {
|
|
18
|
+
if (!this.listeners.has(event)) {
|
|
19
|
+
this.listeners.set(event, new Set());
|
|
20
|
+
}
|
|
21
|
+
this.listeners.get(event).add(callback);
|
|
22
|
+
|
|
23
|
+
// Return unsubscribe function pattern
|
|
24
|
+
return () => this.off(event, callback);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Unsubscribe from an event.
|
|
29
|
+
* @param {string} event
|
|
30
|
+
* @param {Function} callback
|
|
31
|
+
*/
|
|
32
|
+
off(event, callback) {
|
|
33
|
+
if (this.listeners.has(event)) {
|
|
34
|
+
this.listeners.get(event).delete(callback);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Subscribe to an event only once.
|
|
40
|
+
* @param {string} event
|
|
41
|
+
* @param {Function} callback
|
|
42
|
+
*/
|
|
43
|
+
once(event, callback) {
|
|
44
|
+
const wrapper = (...args) => {
|
|
45
|
+
callback(...args);
|
|
46
|
+
this.off(event, wrapper);
|
|
47
|
+
};
|
|
48
|
+
this.on(event, wrapper);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Emit an event with optional data.
|
|
53
|
+
* @param {string} event
|
|
54
|
+
* @param {*} [data]
|
|
55
|
+
*/
|
|
56
|
+
emit(event, data) {
|
|
57
|
+
if (this.listeners.has(event)) {
|
|
58
|
+
this.listeners.get(event).forEach(cb => {
|
|
59
|
+
try {
|
|
60
|
+
cb(data);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(`Error in listener for event "${event}":`, e);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Clears all listeners. Used for disposal.
|
|
70
|
+
*/
|
|
71
|
+
dispose() {
|
|
72
|
+
this.listeners.clear();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -131,4 +131,28 @@ export class PostProcessingManager {
|
|
|
131
131
|
this.bloomPass.radius = Number(rad);
|
|
132
132
|
this.bloomPass.threshold = Number(thr);
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
dispose() {
|
|
136
|
+
if (this.bloomComposer) {
|
|
137
|
+
this.bloomComposer.renderTarget1.dispose();
|
|
138
|
+
this.bloomComposer.renderTarget2.dispose();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.finalComposer) {
|
|
142
|
+
this.finalComposer.renderTarget1.dispose();
|
|
143
|
+
this.finalComposer.renderTarget2.dispose();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (this.bloomPass) {
|
|
147
|
+
this.bloomPass.dispose();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (this.outlinePass) {
|
|
151
|
+
this.outlinePass.dispose();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (this.mixPass && this.mixPass.material) {
|
|
155
|
+
this.mixPass.material.dispose();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
134
158
|
}
|
|
@@ -7,19 +7,32 @@ export class SceneSetup {
|
|
|
7
7
|
constructor(scene) {
|
|
8
8
|
this.scene = scene;
|
|
9
9
|
this.gridHelper = null;
|
|
10
|
+
|
|
11
|
+
this.ambientLight = null;
|
|
12
|
+
this.hemiLight = null;
|
|
13
|
+
this.dirLightMain = null;
|
|
14
|
+
this.dirLightFill = null;
|
|
15
|
+
|
|
10
16
|
this.initLights();
|
|
11
17
|
this.initHelpers();
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
initLights() {
|
|
15
|
-
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// 1. Global Illumination
|
|
22
|
+
this.ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
|
|
23
|
+
this.scene.add(this.ambientLight);
|
|
24
|
+
|
|
25
|
+
this.hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.5);
|
|
26
|
+
this.scene.add(this.hemiLight);
|
|
27
|
+
|
|
28
|
+
// 2. Directional Lights (Shadows & Definition)
|
|
29
|
+
this.dirLightMain = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
30
|
+
this.dirLightMain.position.set(10, 20, 10);
|
|
31
|
+
this.scene.add(this.dirLightMain);
|
|
32
|
+
|
|
33
|
+
this.dirLightFill = new THREE.DirectionalLight(0xffffff, 0.4);
|
|
34
|
+
this.dirLightFill.position.set(0, 0, 20);
|
|
35
|
+
this.scene.add(this.dirLightFill);
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
initHelpers() {
|
|
@@ -29,4 +42,32 @@ export class SceneSetup {
|
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
setGridVisible(vis) { if(this.gridHelper) this.gridHelper.visible = vis; }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Updates the intensity of scene lights.
|
|
48
|
+
* @param {Object} config
|
|
49
|
+
* @param {number} [config.global] - Intensity of Ambient/Hemi lights (0.0 - 2.0)
|
|
50
|
+
* @param {number} [config.main] - Intensity of Main Directional Light (0.0 - 2.0)
|
|
51
|
+
* @param {number} [config.fill] - Intensity of Fill Light (0.0 - 2.0)
|
|
52
|
+
*/
|
|
53
|
+
setLightConfig(config) {
|
|
54
|
+
if (config.global !== undefined) {
|
|
55
|
+
this.ambientLight.intensity = config.global;
|
|
56
|
+
this.hemiLight.intensity = config.global * 0.6;
|
|
57
|
+
}
|
|
58
|
+
if (config.main !== undefined) {
|
|
59
|
+
this.dirLightMain.intensity = config.main;
|
|
60
|
+
}
|
|
61
|
+
if (config.fill !== undefined) {
|
|
62
|
+
this.dirLightFill.intensity = config.fill;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getLightConfig() {
|
|
67
|
+
return {
|
|
68
|
+
global: this.ambientLight.intensity,
|
|
69
|
+
main: this.dirLightMain.intensity,
|
|
70
|
+
fill: this.dirLightFill.intensity
|
|
71
|
+
};
|
|
72
|
+
}
|
|
32
73
|
}
|
package/src/objects/SkinModel.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUti
|
|
|
3
3
|
import { applySkinUVs } from '../utils/SkinUtils.js';
|
|
4
4
|
import { createVoxelLayer } from '../utils/Voxelizer.js';
|
|
5
5
|
import { createGlowMaterial } from '../materials/GlowMaterial.js';
|
|
6
|
+
import {disposeObjectTree} from "../utils/ThreeUtils.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Represents the Minecraft Character Model (Steve/Alex).
|
|
@@ -16,6 +17,8 @@ export class SkinModel {
|
|
|
16
17
|
this.bodyMeshes = [];
|
|
17
18
|
this.defaultPositions = {};
|
|
18
19
|
this.blackMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
20
|
+
|
|
21
|
+
this.LAYERS_COUNT = 20;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -59,15 +62,30 @@ export class SkinModel {
|
|
|
59
62
|
this.bodyMeshes.push(voxelMesh);
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
// 3. Glow
|
|
65
|
+
// 3. Glow Meshes (Multi-Layer Shells)
|
|
63
66
|
const glowParts = [innerGeo.clone()];
|
|
64
67
|
if (voxelGeo) glowParts.push(voxelGeo.clone());
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
this.
|
|
70
|
-
|
|
68
|
+
const baseGlowGeo = BufferGeometryUtils.mergeGeometries(glowParts, false);
|
|
69
|
+
|
|
70
|
+
const partLayers = [];
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < this.LAYERS_COUNT; i++) {
|
|
73
|
+
const glowMat = createGlowMaterial(size.h);
|
|
74
|
+
|
|
75
|
+
glowMat.uniforms.thickness.value = 0;
|
|
76
|
+
glowMat.uniforms.opacity.value = 0;
|
|
77
|
+
|
|
78
|
+
const layerMesh = new THREE.Mesh(baseGlowGeo, glowMat);
|
|
79
|
+
|
|
80
|
+
layerMesh.userData.layerIndex = i;
|
|
81
|
+
layerMesh.userData.isGlow = true;
|
|
82
|
+
layerMesh.userData.glowMat = glowMat;
|
|
83
|
+
|
|
84
|
+
meshGroup.add(layerMesh);
|
|
85
|
+
partLayers.push(layerMesh);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.glowMeshes.push(partLayers);
|
|
71
89
|
|
|
72
90
|
pivotGroup.add(meshGroup);
|
|
73
91
|
return pivotGroup;
|
|
@@ -79,7 +97,24 @@ export class SkinModel {
|
|
|
79
97
|
* @param {boolean} isSlim - True for Alex model (3px arms), False for Steve (4px arms).
|
|
80
98
|
*/
|
|
81
99
|
build(texture, isSlim = false) {
|
|
82
|
-
|
|
100
|
+
let capeBackup = null;
|
|
101
|
+
if (this.parts.cape) {
|
|
102
|
+
const mesh = this.parts.cape.children.find(c => c.isMesh);
|
|
103
|
+
if (mesh && mesh.material.map) {
|
|
104
|
+
capeBackup = {
|
|
105
|
+
texture: mesh.material.map,
|
|
106
|
+
position: this.parts.cape.position.clone(),
|
|
107
|
+
rotation: this.parts.cape.rotation.clone(),
|
|
108
|
+
scale: this.parts.cape.scale.clone()
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (this.playerGroup.children.length > 0) {
|
|
114
|
+
disposeObjectTree(this.playerGroup);
|
|
115
|
+
this.playerGroup.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
83
118
|
this.parts = {};
|
|
84
119
|
this.glowMeshes = [];
|
|
85
120
|
this.bodyMeshes = [];
|
|
@@ -102,13 +137,132 @@ export class SkinModel {
|
|
|
102
137
|
this.parts[name] = part;
|
|
103
138
|
this.playerGroup.add(part);
|
|
104
139
|
}
|
|
140
|
+
|
|
141
|
+
if (capeBackup) {
|
|
142
|
+
this.setCape(capeBackup.texture);
|
|
143
|
+
|
|
144
|
+
if (this.parts.cape) {
|
|
145
|
+
this.parts.cape.position.copy(capeBackup.position);
|
|
146
|
+
this.parts.cape.rotation.copy(capeBackup.rotation);
|
|
147
|
+
this.parts.cape.scale.copy(capeBackup.scale);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Adds or updates the Cape mesh.
|
|
154
|
+
* @param {THREE.Texture} texture
|
|
155
|
+
*/
|
|
156
|
+
setCape(texture) {
|
|
157
|
+
if (this.parts.cape) {
|
|
158
|
+
if (this.parts.cape.userData.glowLayers) {
|
|
159
|
+
const layersToRemove = this.parts.cape.userData.glowLayers;
|
|
160
|
+
this.glowMeshes = this.glowMeshes.filter(layers => layers !== layersToRemove);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.playerGroup.remove(this.parts.cape);
|
|
164
|
+
disposeObjectTree(this.parts.cape);
|
|
165
|
+
delete this.parts.cape;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!texture) return;
|
|
169
|
+
|
|
170
|
+
const size = { w: 10, h: 16, d: 1 };
|
|
171
|
+
const pivotPos = new THREE.Vector3(0, 0, -3);
|
|
172
|
+
const meshOffset = new THREE.Vector3(0, -8, 0.6);
|
|
173
|
+
|
|
174
|
+
const pivotGroup = new THREE.Group();
|
|
175
|
+
pivotGroup.position.copy(pivotPos);
|
|
176
|
+
pivotGroup.name = 'cape';
|
|
177
|
+
pivotGroup.rotation.x = 0.2;
|
|
178
|
+
|
|
179
|
+
this.defaultPositions['cape'] = pivotPos.clone();
|
|
180
|
+
|
|
181
|
+
const geo = new THREE.BoxGeometry(size.w, size.h, size.d);
|
|
182
|
+
applySkinUVs(geo, 0, 0, 10, 16, 1, 64, 32);
|
|
183
|
+
|
|
184
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
185
|
+
map: texture,
|
|
186
|
+
side: THREE.DoubleSide,
|
|
187
|
+
transparent: true,
|
|
188
|
+
alphaTest: 0.5
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const mainMesh = new THREE.Mesh(geo, mat);
|
|
192
|
+
mainMesh.position.copy(meshOffset);
|
|
193
|
+
mainMesh.rotation.y = Math.PI;
|
|
194
|
+
mainMesh.userData.originalMat = mat;
|
|
195
|
+
|
|
196
|
+
pivotGroup.add(mainMesh);
|
|
197
|
+
this.bodyMeshes.push(mainMesh);
|
|
198
|
+
|
|
199
|
+
const capeLayers = [];
|
|
200
|
+
const shellGeo = geo.clone();
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < this.LAYERS_COUNT; i++) {
|
|
203
|
+
const glowMat = createGlowMaterial(size.h);
|
|
204
|
+
|
|
205
|
+
glowMat.uniforms.thickness.value = 0;
|
|
206
|
+
glowMat.uniforms.opacity.value = 0;
|
|
207
|
+
glowMat.polygonOffset = true;
|
|
208
|
+
glowMat.polygonOffsetFactor = i * 0.1;
|
|
209
|
+
|
|
210
|
+
const layerMesh = new THREE.Mesh(shellGeo, glowMat);
|
|
211
|
+
|
|
212
|
+
layerMesh.position.copy(meshOffset);
|
|
213
|
+
layerMesh.rotation.y = Math.PI;
|
|
214
|
+
|
|
215
|
+
layerMesh.userData.layerIndex = i;
|
|
216
|
+
layerMesh.userData.isGlow = true;
|
|
217
|
+
layerMesh.userData.glowMat = glowMat;
|
|
218
|
+
|
|
219
|
+
pivotGroup.add(layerMesh);
|
|
220
|
+
capeLayers.push(layerMesh);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.glowMeshes.push(capeLayers);
|
|
224
|
+
pivotGroup.userData.glowLayers = capeLayers;
|
|
225
|
+
|
|
226
|
+
this.playerGroup.add(pivotGroup);
|
|
227
|
+
this.parts['cape'] = pivotGroup;
|
|
105
228
|
}
|
|
106
229
|
|
|
107
230
|
getGroup() { return this.playerGroup; }
|
|
108
231
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Updates thickness creating a solid volume effect.
|
|
234
|
+
* @param {number} v - Base thickness value.
|
|
235
|
+
*/
|
|
236
|
+
updateBorderThickness(v) {
|
|
237
|
+
const maxThickness = v * 0.05;
|
|
238
|
+
|
|
239
|
+
this.glowMeshes.forEach(layers => {
|
|
240
|
+
layers.forEach((mesh, i) => {
|
|
241
|
+
const progress = (i + 1) / this.LAYERS_COUNT;
|
|
242
|
+
|
|
243
|
+
mesh.userData.glowMat.uniforms.thickness.value = maxThickness * progress;
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
updateGlowHeight(p) {
|
|
249
|
+
this.glowMeshes.forEach(layers => {
|
|
250
|
+
layers.forEach(m => m.userData.glowMat.uniforms.gradientLimit.value = p);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setGlowEffect(en) {
|
|
255
|
+
this.glowMeshes.forEach(layers => {
|
|
256
|
+
layers.forEach((mesh, i) => {
|
|
257
|
+
if (!en) {
|
|
258
|
+
mesh.userData.glowMat.uniforms.opacity.value = 0.0;
|
|
259
|
+
} else {
|
|
260
|
+
mesh.userData.glowMat.uniforms.opacity.value = 1.0 / (this.LAYERS_COUNT * 0.6);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
112
266
|
darkenBody() { this.bodyMeshes.forEach(m => m.material = this.blackMaterial); }
|
|
113
267
|
restoreBody() { this.bodyMeshes.forEach(m => m.material = m.userData.originalMat); }
|
|
114
268
|
|
|
@@ -117,8 +271,22 @@ export class SkinModel {
|
|
|
117
271
|
part.rotation.set(0,0,0);
|
|
118
272
|
if (this.defaultPositions[name]) part.position.copy(this.defaultPositions[name]);
|
|
119
273
|
}
|
|
274
|
+
|
|
275
|
+
this.playerGroup.position.set(0, 0, 0);
|
|
276
|
+
this.playerGroup.rotation.set(0, 0, 0);
|
|
277
|
+
this.playerGroup.scale.set(1, 1, 1);
|
|
278
|
+
|
|
120
279
|
if (!pose) return;
|
|
280
|
+
|
|
281
|
+
if (pose.root) {
|
|
282
|
+
if (pose.root.pos) this.playerGroup.position.fromArray(pose.root.pos);
|
|
283
|
+
if (pose.root.rot) this.playerGroup.rotation.fromArray(pose.root.rot);
|
|
284
|
+
if (pose.root.scl) this.playerGroup.scale.fromArray(pose.root.scl);
|
|
285
|
+
}
|
|
286
|
+
|
|
121
287
|
for (const [name, data] of Object.entries(pose)) {
|
|
288
|
+
if (name === 'root') continue;
|
|
289
|
+
|
|
122
290
|
if (this.parts[name]) {
|
|
123
291
|
if(data.rot) this.parts[name].rotation.set(...data.rot);
|
|
124
292
|
if(data.pos) this.parts[name].position.set(...data.pos);
|
|
@@ -129,9 +297,34 @@ export class SkinModel {
|
|
|
129
297
|
getPose() {
|
|
130
298
|
const pose = {};
|
|
131
299
|
const f = (n) => parseFloat(n.toFixed(3));
|
|
300
|
+
|
|
132
301
|
for (const [name, part] of Object.entries(this.parts)) {
|
|
133
|
-
pose[name] = {
|
|
302
|
+
pose[name] = {
|
|
303
|
+
rot: [f(part.rotation.x), f(part.rotation.y), f(part.rotation.z)],
|
|
304
|
+
pos: [f(part.position.x), f(part.position.y), f(part.position.z)]
|
|
305
|
+
};
|
|
134
306
|
}
|
|
307
|
+
|
|
308
|
+
pose.root = {
|
|
309
|
+
pos: [f(this.playerGroup.position.x), f(this.playerGroup.position.y), f(this.playerGroup.position.z)],
|
|
310
|
+
rot: [f(this.playerGroup.rotation.x), f(this.playerGroup.rotation.y), f(this.playerGroup.rotation.z)],
|
|
311
|
+
scl: [f(this.playerGroup.scale.x), f(this.playerGroup.scale.y), f(this.playerGroup.scale.z)]
|
|
312
|
+
};
|
|
313
|
+
|
|
135
314
|
return pose;
|
|
136
315
|
}
|
|
316
|
+
|
|
317
|
+
dispose() {
|
|
318
|
+
if (this.playerGroup) {
|
|
319
|
+
if (this.playerGroup.parent) {
|
|
320
|
+
this.playerGroup.parent.remove(this.playerGroup);
|
|
321
|
+
}
|
|
322
|
+
disposeObjectTree(this.playerGroup);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.parts = {};
|
|
326
|
+
this.glowMeshes = [];
|
|
327
|
+
this.bodyMeshes = [];
|
|
328
|
+
this.playerGroup = null;
|
|
329
|
+
}
|
|
137
330
|
}
|