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 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
+ }