bucciafico-lib 1.0.4-BETA
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 +178 -0
- package/index.js +5 -0
- package/package.json +39 -0
- package/src/core/SkinViewer.js +195 -0
- package/src/managers/CameraManager.js +52 -0
- package/src/managers/HistoryManager.js +65 -0
- package/src/managers/PostProcessingManager.js +107 -0
- package/src/materials/GlowMaterial.js +56 -0
- package/src/objects/ItemFactory.js +83 -0
- package/src/objects/SceneSetup.js +32 -0
- package/src/objects/SkinModel.js +137 -0
- package/src/plugins/EditorPlugin.js +213 -0
- package/src/plugins/EffectsPlugin.js +149 -0
- package/src/plugins/IOPlugin.js +156 -0
- package/src/plugins/ItemsPlugin.js +71 -0
- package/src/utils/SkinUtils.js +66 -0
- package/src/utils/Voxelizer.js +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Bucciafico Lib
|
|
2
|
+
|
|
3
|
+
bucciafico-lib is a framework-agnostic 3D rendering engine built on top of Three.js. It is designed
|
|
4
|
+
specifically for visualizing, posing, and manipulating Minecraft character skins and items.
|
|
5
|
+
The library features a custom rendering pipeline that includes voxelized outer layers for skins, shader-based glow
|
|
6
|
+
effects, post-processing (bloom, outline), and a robust undo/redo history system for editor implementations.
|
|
7
|
+
|
|
8
|
+
The library utilizes a plugin-based architecture, allowing developers to include only the necessary features (e.g., just the viewer core) or extend it with a full suite of editing tools, post-processing effects, and item management.
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
The repository is organized into the following workspaces:
|
|
12
|
+
|
|
13
|
+
- **Advanced Skin Rendering:** Automatically detects Classic (Steve) and Slim (Alex) models.
|
|
14
|
+
- **Voxelized Outer Layers:** The second layer of the skin (hat, jacket, sleeves) is generated as 3D voxels rather than
|
|
15
|
+
flat planes, providing depth and realism.
|
|
16
|
+
- **Custom Shader Effects:** Includes a specialized shader for creating inner-body glow/backlight effects with
|
|
17
|
+
configurable gradient, strength, and blur.
|
|
18
|
+
- **Item Extrusion:** Procedurally generates 3D meshes from 2D item textures.
|
|
19
|
+
- **Post-Processing Pipeline:** Integrated UnrealBloom and Outline passes for high-quality visuals and object selection
|
|
20
|
+
highlighting.
|
|
21
|
+
- **Editor Tools:** Built-in support for Gizmo controls (Translate, Rotate, Scale), Raycasting, and History management (
|
|
22
|
+
Undo/Redo).
|
|
23
|
+
- **High-Resolution Export:** Capable of rendering high-resolution, transparent PNG screenshots independent of the
|
|
24
|
+
canvas viewport size.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
The library relies on `three` as a peer dependency.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install three
|
|
32
|
+
npm install bucciafico-lib
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Basic Initialization
|
|
38
|
+
If you only need to display a character without interaction or advanced effects:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
import { SkinViewer } from 'bucciafico-lib';
|
|
42
|
+
|
|
43
|
+
const container = document.getElementById('viewer-container');
|
|
44
|
+
|
|
45
|
+
// Initialize Core
|
|
46
|
+
const viewer = new SkinViewer(container, {
|
|
47
|
+
showGrid: true,
|
|
48
|
+
transparent: true,
|
|
49
|
+
cameraEnabled: true
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Load Skin
|
|
53
|
+
viewer.loadSkinByUsername('HappyGFX');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Full Editor Setup
|
|
57
|
+
To enable Gizmos, Glow effects, and Item management, register the respective plugins.
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
import { SkinViewer, EditorPlugin, EffectsPlugin, ItemsPlugin } from 'bucciafico-lib';
|
|
61
|
+
|
|
62
|
+
const viewer = new SkinViewer(document.getElementById('app'));
|
|
63
|
+
|
|
64
|
+
// Initialize Plugins
|
|
65
|
+
const editor = new EditorPlugin();
|
|
66
|
+
const effects = new EffectsPlugin();
|
|
67
|
+
const items = new ItemsPlugin();
|
|
68
|
+
const io = new IOPlugin();
|
|
69
|
+
|
|
70
|
+
// Register Plugins
|
|
71
|
+
viewer.addPlugin(editor);
|
|
72
|
+
viewer.addPlugin(effects);
|
|
73
|
+
viewer.addPlugin(items);
|
|
74
|
+
viewer.addPlugin(io);
|
|
75
|
+
|
|
76
|
+
// Load Skin
|
|
77
|
+
viewer.loadSkinByUsername('Notch');
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Configuration Options
|
|
81
|
+
|
|
82
|
+
The `SkinViewer` constructor accepts a configuration object:
|
|
83
|
+
|
|
84
|
+
| Option | Type | Default | Description |
|
|
85
|
+
|-----------------|---------|------------|---------------------------------------------------------------|
|
|
86
|
+
| `showGrid` | boolean | `true` | Toggles the visibility of the ground grid helper. |
|
|
87
|
+
| `transparent` | boolean | `false` | If true, the canvas background is transparent (alpha 0). |
|
|
88
|
+
| `bgColor` | number | `0x141417` | Hex color of the background if transparency is disabled. |
|
|
89
|
+
| `cameraEnabled` | boolean | `true` | Enables or disables mouse interaction with the camera. |
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
### Skin Loading
|
|
94
|
+
```javascript
|
|
95
|
+
// Load skin from URL (returns Promise<boolean> isSlim)
|
|
96
|
+
viewer.loadSkin('path/to/skin.png');
|
|
97
|
+
|
|
98
|
+
// Load by Username
|
|
99
|
+
viewer.loadSkinByUsername('Notch');
|
|
100
|
+
|
|
101
|
+
// Set Pose (Rotation in radians)
|
|
102
|
+
viewer.setPose({
|
|
103
|
+
head: { rot: [0.2, 0, 0] },
|
|
104
|
+
leftArm: { rot: [-0.5, 0, 0] }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Get Plugin Instance
|
|
108
|
+
const editor = viewer.getPlugin('EditorPlugin');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Editor
|
|
112
|
+
```javascript
|
|
113
|
+
const editor = viewer.getPlugin('EditorPlugin');
|
|
114
|
+
|
|
115
|
+
// Change Gizmo Mode
|
|
116
|
+
editor.setTransformMode('rotate'); // 'translate', 'rotate', 'scale'
|
|
117
|
+
|
|
118
|
+
// Selection
|
|
119
|
+
editor.deselect(); // Clear selection
|
|
120
|
+
|
|
121
|
+
// History
|
|
122
|
+
editor.undo();
|
|
123
|
+
editor.redo();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Effects
|
|
127
|
+
```javascript
|
|
128
|
+
const fx = viewer.getPlugin('EffectsPlugin');
|
|
129
|
+
|
|
130
|
+
// Configure Glow
|
|
131
|
+
fx.updateConfig({
|
|
132
|
+
enabled: true,
|
|
133
|
+
strength: 1.5, // Bloom intensity
|
|
134
|
+
radius: 0.4, // Bloom radius
|
|
135
|
+
height: 0.5, // Gradient height (0.0 - 1.0)
|
|
136
|
+
thickness: 4 // Glow thickness
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Capture Screenshot (Transparent PNG)
|
|
140
|
+
const dataUrl = fx.captureScreenshot(1920, 1080);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Items
|
|
144
|
+
```javascript
|
|
145
|
+
const items = viewer.getPlugin('ItemsPlugin');
|
|
146
|
+
|
|
147
|
+
// Add Item
|
|
148
|
+
items.addItem('path/to/sword.png', 'Diamond Sword').then(mesh => {
|
|
149
|
+
console.log('Item added');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Remove Item
|
|
153
|
+
items.removeItem(meshObject);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### IOPlugin
|
|
157
|
+
```javascript
|
|
158
|
+
const io = viewer.getPlugin('IOPlugin');
|
|
159
|
+
|
|
160
|
+
// Export State
|
|
161
|
+
// You can filter what to export using options
|
|
162
|
+
const jsonState = io.exportState({
|
|
163
|
+
skin: true,
|
|
164
|
+
camera: true,
|
|
165
|
+
effects: true,
|
|
166
|
+
pose: true,
|
|
167
|
+
items: true
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Import State (Async)
|
|
171
|
+
io.importState(jsonState).then(() => {
|
|
172
|
+
console.log('Scene restored');
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
MIT License
|
package/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { SkinViewer } from './src/core/SkinViewer.js';
|
|
2
|
+
export { EditorPlugin } from './src/plugins/EditorPlugin.js';
|
|
3
|
+
export { EffectsPlugin } from './src/plugins/EffectsPlugin.js';
|
|
4
|
+
export { ItemsPlugin } from './src/plugins/ItemsPlugin.js';
|
|
5
|
+
export { IOPlugin } from './src/plugins/IOPlugin.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bucciafico-lib",
|
|
3
|
+
"version": "1.0.4-BETA",
|
|
4
|
+
"description": "Modular 3D rendering engine for Minecraft skins based on Three.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"index.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"minecraft",
|
|
20
|
+
"3d",
|
|
21
|
+
"threejs",
|
|
22
|
+
"skin",
|
|
23
|
+
"viewer",
|
|
24
|
+
"renderer"
|
|
25
|
+
],
|
|
26
|
+
"author": "Dawid Maj",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"three": "^0.160.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/HappyGFX/bucciafico-lib.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/HappyGFX/bucciafico-lib/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/HappyGFX/bucciafico-lib#readme"
|
|
39
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { CameraManager } from '../managers/CameraManager.js';
|
|
3
|
+
import { SceneSetup } from '../objects/SceneSetup.js';
|
|
4
|
+
import { SkinModel } from '../objects/SkinModel.js';
|
|
5
|
+
import { detectSlimSkin } from '../utils/SkinUtils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Core 3D Viewer class.
|
|
9
|
+
* Lightweight, modular engine that handles the basic Three.js scene, camera, and character model.
|
|
10
|
+
* Extended functionality (Editor, Effects, Items) is added via Plugins.
|
|
11
|
+
*/
|
|
12
|
+
export class SkinViewer {
|
|
13
|
+
/**
|
|
14
|
+
* @param {HTMLElement} containerElement - The DOM element to attach the canvas to.
|
|
15
|
+
* @param {Object} [config] - Basic configuration.
|
|
16
|
+
* @param {boolean} [config.showGrid=true] - Visibility of the floor grid.
|
|
17
|
+
* @param {boolean} [config.transparent=false] - Background transparency.
|
|
18
|
+
* @param {number} [config.bgColor=0x141417] - Background hex color.
|
|
19
|
+
* @param {boolean} [config.cameraEnabled=true] - OrbitControls state.
|
|
20
|
+
*/
|
|
21
|
+
constructor(containerElement, config = {}) {
|
|
22
|
+
this.container = containerElement;
|
|
23
|
+
this.config = {
|
|
24
|
+
showGrid: config.showGrid ?? true,
|
|
25
|
+
transparent: config.transparent ?? false,
|
|
26
|
+
bgColor: config.bgColor ?? 0x141417,
|
|
27
|
+
cameraEnabled: config.cameraEnabled ?? true,
|
|
28
|
+
...config
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** @type {Map<string, Object>} Registered plugins. */
|
|
32
|
+
this.plugins = new Map();
|
|
33
|
+
|
|
34
|
+
/** @type {boolean} Flag to stop the animation loop. */
|
|
35
|
+
this.isDisposed = false;
|
|
36
|
+
|
|
37
|
+
// --- 1. RENDERER SETUP ---
|
|
38
|
+
this.renderer = new THREE.WebGLRenderer({
|
|
39
|
+
antialias: true,
|
|
40
|
+
alpha: this.config.transparent,
|
|
41
|
+
preserveDrawingBuffer: true
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const w = this.container.clientWidth;
|
|
45
|
+
const h = this.container.clientHeight;
|
|
46
|
+
|
|
47
|
+
this.renderer.setSize(w, h);
|
|
48
|
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
49
|
+
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
50
|
+
this.renderer.autoClear = false; // Required for post-processing plugins
|
|
51
|
+
|
|
52
|
+
this.container.appendChild(this.renderer.domElement);
|
|
53
|
+
|
|
54
|
+
// --- 2. SCENE SETUP ---
|
|
55
|
+
this.scene = new THREE.Scene();
|
|
56
|
+
if (!this.config.transparent) {
|
|
57
|
+
this.scene.background = new THREE.Color(this.config.bgColor);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.overlayScene = new THREE.Scene();
|
|
61
|
+
|
|
62
|
+
this.sceneSetup = new SceneSetup(this.scene);
|
|
63
|
+
this.sceneSetup.setGridVisible(this.config.showGrid);
|
|
64
|
+
|
|
65
|
+
this.cameraManager = new CameraManager(this.renderer.domElement, w, h);
|
|
66
|
+
this.cameraManager.setEnabled(this.config.cameraEnabled);
|
|
67
|
+
|
|
68
|
+
this.skinModel = new SkinModel();
|
|
69
|
+
this.scene.add(this.skinModel.getGroup());
|
|
70
|
+
|
|
71
|
+
// --- 3. START LOOP ---
|
|
72
|
+
this.animate = this.animate.bind(this);
|
|
73
|
+
this.animate();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Registers and initializes a plugin.
|
|
78
|
+
* @param {Object} plugin - The plugin instance (must have an init() method).
|
|
79
|
+
* @returns {Object} The registered plugin instance.
|
|
80
|
+
*/
|
|
81
|
+
addPlugin(plugin) {
|
|
82
|
+
const name = plugin.constructor.name;
|
|
83
|
+
if (this.plugins.has(name)) return this.plugins.get(name);
|
|
84
|
+
|
|
85
|
+
if (plugin.init) plugin.init(this);
|
|
86
|
+
this.plugins.set(name, plugin);
|
|
87
|
+
|
|
88
|
+
return plugin;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Retrieves a registered plugin by class name.
|
|
93
|
+
* @param {string} name - e.g., 'EditorPlugin', 'EffectsPlugin'.
|
|
94
|
+
* @returns {Object|undefined}
|
|
95
|
+
*/
|
|
96
|
+
getPlugin(name) {
|
|
97
|
+
return this.plugins.get(name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Loads a skin from URL.
|
|
102
|
+
* @param {string} imageUrl
|
|
103
|
+
* @returns {Promise<boolean>} isSlim
|
|
104
|
+
*/
|
|
105
|
+
loadSkin(imageUrl) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
new THREE.TextureLoader().load(imageUrl, (texture) => {
|
|
108
|
+
texture.magFilter = THREE.NearestFilter;
|
|
109
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
110
|
+
|
|
111
|
+
const currentPose = this.skinModel.getPose();
|
|
112
|
+
const isSlim = detectSlimSkin(texture.image);
|
|
113
|
+
|
|
114
|
+
const editor = this.getPlugin('EditorPlugin');
|
|
115
|
+
if (editor) editor.deselect();
|
|
116
|
+
|
|
117
|
+
this.skinModel.build(texture, isSlim);
|
|
118
|
+
this.skinModel.setPose(currentPose);
|
|
119
|
+
this.skinData = { type: 'url', value: imageUrl };
|
|
120
|
+
|
|
121
|
+
resolve(isSlim);
|
|
122
|
+
}, undefined, reject);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
loadSkinByUsername(username) {
|
|
127
|
+
this.skinData = { type: 'username', value: username };
|
|
128
|
+
const url = `https://minotar.net/skin/${username}.png?v=${Date.now()}`;
|
|
129
|
+
|
|
130
|
+
return this.loadSkin(url).then(res => {
|
|
131
|
+
this.skinData = { type: 'username', value: username };
|
|
132
|
+
return res;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setPose(poseData) {
|
|
137
|
+
// Record history if Editor is present
|
|
138
|
+
const editor = this.getPlugin('EditorPlugin');
|
|
139
|
+
if (editor) editor.saveHistory();
|
|
140
|
+
|
|
141
|
+
this.skinModel.setPose(poseData);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handles window resize. Should be called by the implementation layer.
|
|
146
|
+
*/
|
|
147
|
+
onResize() {
|
|
148
|
+
if (!this.container) return;
|
|
149
|
+
const w = this.container.clientWidth;
|
|
150
|
+
const h = this.container.clientHeight;
|
|
151
|
+
|
|
152
|
+
this.cameraManager.onResize(w, h);
|
|
153
|
+
this.renderer.setSize(w, h);
|
|
154
|
+
|
|
155
|
+
// Notify all plugins
|
|
156
|
+
this.plugins.forEach(p => {
|
|
157
|
+
if (p.onResize) p.onResize(w, h);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
animate() {
|
|
162
|
+
if (this.isDisposed) return;
|
|
163
|
+
requestAnimationFrame(this.animate);
|
|
164
|
+
|
|
165
|
+
this.cameraManager.update();
|
|
166
|
+
|
|
167
|
+
const effects = this.getPlugin('EffectsPlugin');
|
|
168
|
+
|
|
169
|
+
if (effects) {
|
|
170
|
+
effects.render();
|
|
171
|
+
} else {
|
|
172
|
+
this.renderer.clear();
|
|
173
|
+
this.renderer.render(this.scene, this.cameraManager.camera);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.renderer.clearDepth();
|
|
177
|
+
this.renderer.render(this.overlayScene, this.cameraManager.camera);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
dispose() {
|
|
181
|
+
this.isDisposed = true;
|
|
182
|
+
|
|
183
|
+
if (this.container && this.renderer.domElement) {
|
|
184
|
+
this.container.removeChild(this.renderer.domElement);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.renderer.dispose();
|
|
188
|
+
|
|
189
|
+
// Dispose all plugins
|
|
190
|
+
this.plugins.forEach(p => {
|
|
191
|
+
if (p.dispose) p.dispose();
|
|
192
|
+
});
|
|
193
|
+
this.plugins.clear();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps Three.js Camera and OrbitControls.
|
|
6
|
+
*/
|
|
7
|
+
export class CameraManager {
|
|
8
|
+
constructor(domElement, width, height) {
|
|
9
|
+
this.defaultFOV = 45;
|
|
10
|
+
this.defaultPosition = new THREE.Vector3(20, 10, 40);
|
|
11
|
+
this.defaultTarget = new THREE.Vector3(0, 0, 0);
|
|
12
|
+
|
|
13
|
+
this.camera = new THREE.PerspectiveCamera(this.defaultFOV, width / height, 0.1, 1000);
|
|
14
|
+
this.camera.position.copy(this.defaultPosition);
|
|
15
|
+
|
|
16
|
+
this.controls = new OrbitControls(this.camera, domElement);
|
|
17
|
+
this.controls.enableDamping = true;
|
|
18
|
+
this.controls.dampingFactor = 0.05;
|
|
19
|
+
this.controls.target.copy(this.defaultTarget);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
update() { this.controls.update(); }
|
|
23
|
+
onResize(width, height) { this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); }
|
|
24
|
+
setFOV(value) { this.camera.fov = value; this.camera.updateProjectionMatrix(); }
|
|
25
|
+
setDistance(distance) {
|
|
26
|
+
const direction = new THREE.Vector3().subVectors(this.camera.position, this.controls.target).normalize();
|
|
27
|
+
this.camera.position.copy(this.controls.target).add(direction.multiplyScalar(distance));
|
|
28
|
+
}
|
|
29
|
+
setEnabled(enabled) { this.controls.enabled = enabled; }
|
|
30
|
+
reset() {
|
|
31
|
+
this.setFOV(this.defaultFOV);
|
|
32
|
+
this.camera.position.copy(this.defaultPosition);
|
|
33
|
+
this.controls.target.copy(this.defaultTarget);
|
|
34
|
+
this.controls.update();
|
|
35
|
+
}
|
|
36
|
+
getSettingsJSON() {
|
|
37
|
+
const r = (val) => parseFloat(val.toFixed(3));
|
|
38
|
+
const rVec = (v) => [r(v.x), r(v.y), r(v.z)];
|
|
39
|
+
return {
|
|
40
|
+
fov: this.camera.fov,
|
|
41
|
+
zoom: r(this.camera.position.distanceTo(this.controls.target)),
|
|
42
|
+
position: rVec(this.camera.position),
|
|
43
|
+
target: rVec(this.controls.target)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
loadSettingsJSON(data) {
|
|
47
|
+
if (data.fov) { this.camera.fov = data.fov; this.camera.updateProjectionMatrix(); }
|
|
48
|
+
if (data.position) this.camera.position.fromArray(data.position);
|
|
49
|
+
if (data.target) this.controls.target.fromArray(data.target);
|
|
50
|
+
this.controls.update();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages the Undo/Redo stack for the editor.
|
|
3
|
+
* Handles state snapshots including poses and item transformations.
|
|
4
|
+
*/
|
|
5
|
+
export class HistoryManager {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Function} applyStateCallback - Function to call when a state needs to be restored.
|
|
8
|
+
*/
|
|
9
|
+
constructor(applyStateCallback) {
|
|
10
|
+
this.undoStack = [];
|
|
11
|
+
this.redoStack = [];
|
|
12
|
+
this.applyState = applyStateCallback;
|
|
13
|
+
this.maxHistory = 50;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pushes a new state snapshot to the history stack.
|
|
18
|
+
* Clears the Redo stack as a new timeline is created.
|
|
19
|
+
* @param {Object} state - The snapshot object.
|
|
20
|
+
*/
|
|
21
|
+
pushState(state) {
|
|
22
|
+
this.redoStack = [];
|
|
23
|
+
const stateStr = JSON.stringify(state);
|
|
24
|
+
|
|
25
|
+
// Don't save if state hasn't changed
|
|
26
|
+
if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === stateStr) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.undoStack.push(stateStr);
|
|
31
|
+
if (this.undoStack.length > this.maxHistory) {
|
|
32
|
+
this.undoStack.shift();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reverts to the previous state.
|
|
38
|
+
* @param {Object} currentState - The current state (to save into Redo before undoing).
|
|
39
|
+
*/
|
|
40
|
+
undo(currentState) {
|
|
41
|
+
if (this.undoStack.length === 0) return;
|
|
42
|
+
|
|
43
|
+
this.redoStack.push(JSON.stringify(currentState));
|
|
44
|
+
const prevStateStr = this.undoStack.pop();
|
|
45
|
+
|
|
46
|
+
if (prevStateStr) {
|
|
47
|
+
this.applyState(JSON.parse(prevStateStr));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reapplies a previously undone state.
|
|
53
|
+
* @param {Object} currentState - The current state (to save into Undo before redoing).
|
|
54
|
+
*/
|
|
55
|
+
redo(currentState) {
|
|
56
|
+
if (this.redoStack.length === 0) return;
|
|
57
|
+
|
|
58
|
+
this.undoStack.push(JSON.stringify(currentState));
|
|
59
|
+
const nextStateStr = this.redoStack.pop();
|
|
60
|
+
|
|
61
|
+
if (nextStateStr) {
|
|
62
|
+
this.applyState(JSON.parse(nextStateStr));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
|
|
3
|
+
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
|
|
4
|
+
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
|
|
5
|
+
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
|
|
6
|
+
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
|
|
7
|
+
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Handles the post-processing pipeline (Bloom, Outline, Color Correction).
|
|
11
|
+
*/
|
|
12
|
+
export class PostProcessingManager {
|
|
13
|
+
constructor(renderer, scene, camera, width, height) {
|
|
14
|
+
this.scene = scene;
|
|
15
|
+
|
|
16
|
+
// 1. BLOOM COMPOSER (Renders glow map)
|
|
17
|
+
this.bloomComposer = new EffectComposer(renderer);
|
|
18
|
+
this.bloomComposer.renderToScreen = false;
|
|
19
|
+
this.bloomComposer.addPass(new RenderPass(scene, camera));
|
|
20
|
+
|
|
21
|
+
this.bloomPass = new UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.0);
|
|
22
|
+
this.bloomComposer.addPass(this.bloomPass);
|
|
23
|
+
|
|
24
|
+
// 2. FINAL COMPOSER
|
|
25
|
+
this.finalComposer = new EffectComposer(renderer);
|
|
26
|
+
this.finalComposer.addPass(new RenderPass(scene, camera));
|
|
27
|
+
|
|
28
|
+
// 3. OUTLINE PASS (Selection highlight)
|
|
29
|
+
this.outlinePass = new OutlinePass(new THREE.Vector2(width, height), scene, camera);
|
|
30
|
+
this.outlinePass.edgeStrength = 3.0;
|
|
31
|
+
this.outlinePass.visibleEdgeColor.set('#ffffff');
|
|
32
|
+
this.outlinePass.hiddenEdgeColor.set('#ffffff');
|
|
33
|
+
this.finalComposer.addPass(this.outlinePass);
|
|
34
|
+
|
|
35
|
+
// 4. MIX SHADER (Combines Base + Bloom preserving Alpha)
|
|
36
|
+
const MixShader = {
|
|
37
|
+
uniforms: {
|
|
38
|
+
tDiffuse: { value: null },
|
|
39
|
+
bloomTexture: { value: null }
|
|
40
|
+
},
|
|
41
|
+
vertexShader: `
|
|
42
|
+
varying vec2 vUv;
|
|
43
|
+
void main() {
|
|
44
|
+
vUv = uv;
|
|
45
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
fragmentShader: `
|
|
49
|
+
uniform sampler2D tDiffuse;
|
|
50
|
+
uniform sampler2D bloomTexture;
|
|
51
|
+
varying vec2 vUv;
|
|
52
|
+
void main() {
|
|
53
|
+
vec4 baseColor = texture2D(tDiffuse, vUv);
|
|
54
|
+
vec4 bloomColor = texture2D(bloomTexture, vUv);
|
|
55
|
+
|
|
56
|
+
// Add only colors (RGB), keep the Base Alpha.
|
|
57
|
+
// This prevents the black background of the bloom pass from overwriting transparency.
|
|
58
|
+
gl_FragColor = vec4(baseColor.rgb + (bloomColor.rgb * 2.5), baseColor.a);
|
|
59
|
+
}
|
|
60
|
+
`
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.mixPass = new ShaderPass(MixShader);
|
|
64
|
+
this.mixPass.needsSwap = true;
|
|
65
|
+
this.finalComposer.addPass(this.mixPass);
|
|
66
|
+
|
|
67
|
+
// 5. OUTPUT PASS (sRGB correction)
|
|
68
|
+
this.finalComposer.addPass(new OutputPass());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resize(width, height) {
|
|
72
|
+
this.bloomComposer.setSize(width, height);
|
|
73
|
+
this.finalComposer.setSize(width, height);
|
|
74
|
+
this.bloomPass.resolution.set(width, height);
|
|
75
|
+
this.outlinePass.setSize(width, height);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Renders the scene in two passes to achieve the glow effect on specific objects only.
|
|
80
|
+
* @param {Function} prepareBloomCb - Callback to hide non-glowing objects.
|
|
81
|
+
* @param {Function} restoreSceneCb - Callback to restore visibility.
|
|
82
|
+
*/
|
|
83
|
+
renderSelective(prepareBloomCb, restoreSceneCb) {
|
|
84
|
+
const prevBg = this.scene.background;
|
|
85
|
+
this.scene.background = new THREE.Color(0x000000);
|
|
86
|
+
this.outlinePass.enabled = false;
|
|
87
|
+
|
|
88
|
+
prepareBloomCb();
|
|
89
|
+
this.bloomComposer.render();
|
|
90
|
+
restoreSceneCb();
|
|
91
|
+
|
|
92
|
+
this.scene.background = prevBg;
|
|
93
|
+
this.outlinePass.enabled = true;
|
|
94
|
+
this.mixPass.uniforms.bloomTexture.value = this.bloomComposer.readBuffer.texture;
|
|
95
|
+
this.finalComposer.render();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setSelected(obj) {
|
|
99
|
+
this.outlinePass.selectedObjects = obj ? [obj] : [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setBloom(en, str, rad, thr) {
|
|
103
|
+
this.bloomPass.strength = en ? Number(str) : 0;
|
|
104
|
+
this.bloomPass.radius = Number(rad);
|
|
105
|
+
this.bloomPass.threshold = Number(thr);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
const vertexShader = `
|
|
4
|
+
uniform float thickness;
|
|
5
|
+
varying float vY;
|
|
6
|
+
varying vec3 vNormal;
|
|
7
|
+
void main() {
|
|
8
|
+
vNormal = normal;
|
|
9
|
+
// Extrude vertex along normal
|
|
10
|
+
vec3 newPos = position + normal * thickness;
|
|
11
|
+
vY = position.y;
|
|
12
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const fragmentShader = `
|
|
17
|
+
uniform float opacity;
|
|
18
|
+
uniform float gradientLimit;
|
|
19
|
+
uniform float partHeight;
|
|
20
|
+
varying float vY;
|
|
21
|
+
varying vec3 vNormal;
|
|
22
|
+
void main() {
|
|
23
|
+
if (opacity <= 0.01) discard;
|
|
24
|
+
// Discard bottom faces to avoid "floor" artifact
|
|
25
|
+
if (vNormal.y < -0.9) discard;
|
|
26
|
+
|
|
27
|
+
// Gradient logic
|
|
28
|
+
float normalizedY = (vY + (partHeight / 2.0)) / partHeight;
|
|
29
|
+
float alpha = smoothstep(1.0 - gradientLimit, 1.0, normalizedY);
|
|
30
|
+
alpha *= smoothstep(0.0, 0.2, normalizedY);
|
|
31
|
+
|
|
32
|
+
gl_FragColor = vec4(1.0, 1.0, 1.0, alpha * opacity);
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a ShaderMaterial for the glow effect.
|
|
38
|
+
* It renders a larger, back-side version of the mesh with an opacity gradient.
|
|
39
|
+
* @param {number} partHeight - Height of the body part for gradient calculation.
|
|
40
|
+
*/
|
|
41
|
+
export function createGlowMaterial(partHeight) {
|
|
42
|
+
return new THREE.ShaderMaterial({
|
|
43
|
+
uniforms: {
|
|
44
|
+
opacity: { value: 0.0 },
|
|
45
|
+
gradientLimit: { value: 0.8 },
|
|
46
|
+
thickness: { value: 0.0 },
|
|
47
|
+
partHeight: { value: partHeight }
|
|
48
|
+
},
|
|
49
|
+
vertexShader,
|
|
50
|
+
fragmentShader,
|
|
51
|
+
transparent: true,
|
|
52
|
+
side: THREE.BackSide, // Render inside of the extruded box
|
|
53
|
+
depthWrite: false,
|
|
54
|
+
blending: THREE.AdditiveBlending
|
|
55
|
+
});
|
|
56
|
+
}
|