@joshtol/emotive-engine 3.3.3 → 3.3.5
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/dist/emotive-mascot-3d.js +1 -1
- package/dist/emotive-mascot-3d.js.map +1 -1
- package/dist/emotive-mascot-3d.umd.js +1 -1
- package/dist/emotive-mascot-3d.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/3d/Core3DManager.js +13 -9
- package/src/3d/ThreeRenderer.js +29 -15
- package/src/3d/effects/CrystalSoul.js +13 -19
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@joshtol/emotive-engine",
|
|
4
|
-
"version": "3.3.
|
|
4
|
+
"version": "3.3.5",
|
|
5
5
|
"description": "Open-source animation engine for AI-controlled emotional visualizations with musical time synchronization",
|
|
6
6
|
"main": "dist/emotive-mascot.umd.js",
|
|
7
7
|
"module": "dist/mascot.js",
|
package/src/3d/Core3DManager.js
CHANGED
|
@@ -291,6 +291,8 @@ export class Core3DManager {
|
|
|
291
291
|
this.glowColor = [1.0, 1.0, 1.0]; // RGB
|
|
292
292
|
this.glowColorHex = '#FFFFFF'; // Hex color for luminance normalization
|
|
293
293
|
this.glowIntensity = 1.0;
|
|
294
|
+
// OPTIMIZATION: Cache normalized color to avoid recalculating every frame
|
|
295
|
+
this._normalizedGlowColor = null;
|
|
294
296
|
this.coreGlowEnabled = true; // Toggle to enable/disable core glow
|
|
295
297
|
this.glowIntensityOverride = null; // Manual override for testing
|
|
296
298
|
this.intensityCalibrationOffset = 0; // Universal filter calibration offset
|
|
@@ -419,6 +421,9 @@ export class Core3DManager {
|
|
|
419
421
|
this.glowColor = rgb;
|
|
420
422
|
// Store hex color for bloom luminance normalization
|
|
421
423
|
this.glowColorHex = emotionData.visual.glowColor;
|
|
424
|
+
// OPTIMIZATION: Pre-compute normalized color (avoids recalculating every frame)
|
|
425
|
+
const normalized = normalizeRGBLuminance(rgb, 0.30);
|
|
426
|
+
this._normalizedGlowColor = [normalized.r, normalized.g, normalized.b];
|
|
422
427
|
|
|
423
428
|
// Calculate intensity using universal filter based on color luminance
|
|
424
429
|
// This ensures consistent visibility across all emotions regardless of color brightness
|
|
@@ -1832,11 +1837,9 @@ export class Core3DManager {
|
|
|
1832
1837
|
this.customMaterial.uniforms.glowIntensity.value = effectiveGlowIntensity;
|
|
1833
1838
|
}
|
|
1834
1839
|
|
|
1835
|
-
//
|
|
1840
|
+
// OPTIMIZATION: Use pre-computed normalized color (calculated when emotion changes)
|
|
1836
1841
|
// This ensures yellow (joy) doesn't wash out the soul while blue (sadness) stays visible
|
|
1837
|
-
|
|
1838
|
-
const normalized = normalizeRGBLuminance(this.glowColor, 0.30);
|
|
1839
|
-
const normalizedColor = [normalized.r, normalized.g, normalized.b];
|
|
1842
|
+
const normalizedColor = this._normalizedGlowColor || [1, 1, 1];
|
|
1840
1843
|
|
|
1841
1844
|
// Update emotion color on outer shell (luminance-normalized)
|
|
1842
1845
|
this.customMaterial.uniforms.emotionColor.value.setRGB(
|
|
@@ -1848,12 +1851,10 @@ export class Core3DManager {
|
|
|
1848
1851
|
this.customMaterial.uniforms.blinkIntensity.value = blinkPulse;
|
|
1849
1852
|
}
|
|
1850
1853
|
}
|
|
1851
|
-
// Update inner core color and animation (also use normalized color)
|
|
1854
|
+
// Update inner core color and animation (also use cached normalized color)
|
|
1852
1855
|
// Only update if core glow is enabled
|
|
1853
|
-
// IMPORTANT: Use glowColor (RGB), not glowColorHex - glowColor has undertone saturation applied
|
|
1854
1856
|
if (this.coreGlowEnabled) {
|
|
1855
|
-
const
|
|
1856
|
-
const normalizedCoreColor = [normalizedCore.r, normalizedCore.g, normalizedCore.b];
|
|
1857
|
+
const normalizedCoreColor = this._normalizedGlowColor || [1, 1, 1];
|
|
1857
1858
|
this.updateCrystalInnerCore(normalizedCoreColor, deltaTime);
|
|
1858
1859
|
}
|
|
1859
1860
|
}
|
|
@@ -1878,7 +1879,10 @@ export class Core3DManager {
|
|
|
1878
1879
|
calibrationRotation: this.calibrationRotation, // Applied on top of animated rotation
|
|
1879
1880
|
solarEclipse: this.effectManager.getSolarEclipse(), // Pass eclipse manager for synchronized updates
|
|
1880
1881
|
deltaTime, // Pass deltaTime for eclipse animation
|
|
1881
|
-
morphProgress: morphState.isTransitioning ? morphState.visualProgress : null // For corona fade-in
|
|
1882
|
+
morphProgress: morphState.isTransitioning ? morphState.visualProgress : null, // For corona fade-in
|
|
1883
|
+
// OPTIMIZATION FLAGS: Skip render passes when not needed
|
|
1884
|
+
hasSoul: this.customMaterialType === 'crystal' && this.crystalSoul !== null,
|
|
1885
|
+
hasParticles: this.particleVisibility && this.particleOrchestrator !== null
|
|
1882
1886
|
});
|
|
1883
1887
|
|
|
1884
1888
|
// Update lunar eclipse animation (Blood Moon)
|
package/src/3d/ThreeRenderer.js
CHANGED
|
@@ -143,6 +143,13 @@ export class ThreeRenderer {
|
|
|
143
143
|
this._zAxis = new THREE.Vector3(0, 0, 1);
|
|
144
144
|
this._cameraToMesh = new THREE.Vector3();
|
|
145
145
|
this._cameraDir = new THREE.Vector3();
|
|
146
|
+
|
|
147
|
+
// OPTIMIZATION: Reusable temp vector for soul position projection (avoids allocation per frame)
|
|
148
|
+
this._soulPosTemp = new THREE.Vector3();
|
|
149
|
+
// OPTIMIZATION: Cached soul mesh reference (avoids scene.traverse every frame)
|
|
150
|
+
this._cachedSoulMesh = null;
|
|
151
|
+
// OPTIMIZATION: Reusable Vector2 for drawing buffer size queries
|
|
152
|
+
this._drawingBufferSize = new THREE.Vector2();
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
/**
|
|
@@ -1270,6 +1277,8 @@ export class ThreeRenderer {
|
|
|
1270
1277
|
* @param {number} [params.cameraRoll=0] - Camera-space roll rotation
|
|
1271
1278
|
* @param {SolarEclipse} [params.solarEclipse=null] - Solar eclipse manager for synchronized updates
|
|
1272
1279
|
* @param {number} [params.deltaTime=0] - Delta time for eclipse animation (seconds)
|
|
1280
|
+
* @param {boolean} [params.hasSoul=false] - Whether this geometry has a soul layer (optimization)
|
|
1281
|
+
* @param {boolean} [params.hasParticles=true] - Whether particles are enabled (optimization)
|
|
1273
1282
|
*/
|
|
1274
1283
|
render(params = {}) {
|
|
1275
1284
|
// Guard against calls after destroy
|
|
@@ -1347,7 +1356,9 @@ export class ThreeRenderer {
|
|
|
1347
1356
|
cameraRoll = 0, // Camera-space roll rotation applied after all other rotations
|
|
1348
1357
|
solarEclipse = null, // Solar eclipse manager for synchronized updates
|
|
1349
1358
|
deltaTime = 0, // Delta time for eclipse animation
|
|
1350
|
-
morphProgress = null // Morph progress for corona fade-in (null = no morph, 0-1 = morphing)
|
|
1359
|
+
morphProgress = null, // Morph progress for corona fade-in (null = no morph, 0-1 = morphing)
|
|
1360
|
+
hasSoul = false, // Whether this geometry has a soul layer (skip soul pass if false)
|
|
1361
|
+
hasParticles = true // Whether particles are enabled (skip particle pass if false)
|
|
1351
1362
|
} = params;
|
|
1352
1363
|
|
|
1353
1364
|
// Update camera controls FIRST before any rendering
|
|
@@ -1488,15 +1499,17 @@ export class ThreeRenderer {
|
|
|
1488
1499
|
// Render with post-processing if enabled, otherwise direct render
|
|
1489
1500
|
if (this.composer) {
|
|
1490
1501
|
// === STEP 0: Render soul (layer 2) to texture for refraction sampling ===
|
|
1491
|
-
if
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
this.
|
|
1495
|
-
|
|
1496
|
-
|
|
1502
|
+
// OPTIMIZATION: Skip soul pass entirely if geometry doesn't have a soul
|
|
1503
|
+
if (this.soulRenderTarget && hasSoul) {
|
|
1504
|
+
// OPTIMIZATION: Use cached soul mesh reference instead of traversing every frame
|
|
1505
|
+
if (!this._cachedSoulMesh) {
|
|
1506
|
+
this.scene.traverse(obj => {
|
|
1507
|
+
if (obj.name === 'crystalSoul') this._cachedSoulMesh = obj;
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
const soulMesh = this._cachedSoulMesh;
|
|
1497
1511
|
|
|
1498
1512
|
this.renderer.setRenderTarget(this.soulRenderTarget);
|
|
1499
|
-
this.renderer.setClearColor(0x000000, 0);
|
|
1500
1513
|
this.renderer.clear();
|
|
1501
1514
|
|
|
1502
1515
|
// Render only soul layer (layer 2)
|
|
@@ -1515,17 +1528,17 @@ export class ThreeRenderer {
|
|
|
1515
1528
|
}
|
|
1516
1529
|
// Compute soul's screen center position for refraction sampling
|
|
1517
1530
|
if (this.coreMesh.material.uniforms.soulScreenCenter && soulMesh) {
|
|
1518
|
-
|
|
1519
|
-
|
|
1531
|
+
// OPTIMIZATION: Reuse pooled vector instead of cloning every frame
|
|
1532
|
+
this._soulPosTemp.copy(soulMesh.position);
|
|
1533
|
+
this._soulPosTemp.project(this.camera);
|
|
1520
1534
|
// Convert from NDC (-1 to 1) to UV (0 to 1)
|
|
1521
|
-
const soulScreenU = (
|
|
1522
|
-
const soulScreenV = (
|
|
1535
|
+
const soulScreenU = (this._soulPosTemp.x + 1.0) * 0.5;
|
|
1536
|
+
const soulScreenV = (this._soulPosTemp.y + 1.0) * 0.5;
|
|
1523
1537
|
this.coreMesh.material.uniforms.soulScreenCenter.value.set(soulScreenU, soulScreenV);
|
|
1524
1538
|
}
|
|
1525
1539
|
}
|
|
1526
1540
|
|
|
1527
1541
|
this.renderer.setRenderTarget(null);
|
|
1528
|
-
this.renderer.setClearColor(0x000000, 0);
|
|
1529
1542
|
}
|
|
1530
1543
|
|
|
1531
1544
|
// === STEP 1: Render main scene (layer 0) through bloom to screen ===
|
|
@@ -1533,7 +1546,8 @@ export class ThreeRenderer {
|
|
|
1533
1546
|
this.composer.render();
|
|
1534
1547
|
|
|
1535
1548
|
// === STEP 2: Render particles (layer 1) to separate render target ===
|
|
1536
|
-
if
|
|
1549
|
+
// OPTIMIZATION: Skip particle pass entirely if particles are disabled
|
|
1550
|
+
if (hasParticles && this.particleRenderTarget && this.particleBloomPass) {
|
|
1537
1551
|
// Clear particle render target with WHITE (non-black) to prevent dark halos
|
|
1538
1552
|
this.renderer.setRenderTarget(this.particleRenderTarget);
|
|
1539
1553
|
this.renderer.setClearColor(0xffffff, 0); // White RGB, but 0 alpha
|
|
@@ -1565,7 +1579,7 @@ export class ThreeRenderer {
|
|
|
1565
1579
|
// Reset clear color
|
|
1566
1580
|
this.renderer.setClearColor(0x000000, 0);
|
|
1567
1581
|
this.renderer.setRenderTarget(null);
|
|
1568
|
-
} else {
|
|
1582
|
+
} else if (hasParticles) {
|
|
1569
1583
|
// Fallback: Render particles directly (no bloom)
|
|
1570
1584
|
this.camera.layers.set(1);
|
|
1571
1585
|
this.renderer.render(this.scene, this.camera);
|
|
@@ -95,18 +95,14 @@ const soulFragmentShader = `
|
|
|
95
95
|
|
|
96
96
|
if (driftEnabled > 0.5) {
|
|
97
97
|
float t = time * driftSpeed;
|
|
98
|
-
//
|
|
98
|
+
// OPTIMIZED: Reduced from 4 noise calls to 2 for better performance
|
|
99
|
+
// Primary drift - single noise call with combined coordinates
|
|
99
100
|
float drift1 = noise3D(vPosition * 2.0 + vec3(t, t * 0.7, t * 0.3));
|
|
100
|
-
|
|
101
|
-
// Use max instead of multiply to avoid near-zero products
|
|
102
|
-
primaryDrift = max(drift1, drift2);
|
|
103
|
-
primaryDrift = max(0.0, primaryDrift - 0.4) * 2.0; // Rescale after threshold
|
|
101
|
+
primaryDrift = max(0.0, drift1 - 0.3) * 1.5; // Adjusted threshold for single noise
|
|
104
102
|
|
|
105
|
-
// Secondary drift - offset
|
|
106
|
-
float
|
|
107
|
-
|
|
108
|
-
secondaryDrift = max(drift3, drift4);
|
|
109
|
-
secondaryDrift = max(0.0, secondaryDrift - 0.4) * 2.0;
|
|
103
|
+
// Secondary drift - offset phase to fill gaps
|
|
104
|
+
float drift2 = noise3D(vPosition * 2.5 - vec3(t * 0.6, t * 0.4, t));
|
|
105
|
+
secondaryDrift = max(0.0, drift2 - 0.3) * 1.5;
|
|
110
106
|
|
|
111
107
|
driftEnergy = primaryDrift + secondaryDrift;
|
|
112
108
|
}
|
|
@@ -295,7 +291,7 @@ export class CrystalSoul {
|
|
|
295
291
|
uniforms: {
|
|
296
292
|
time: { value: 0 },
|
|
297
293
|
emotionColor: { value: new THREE.Color(1, 1, 1) },
|
|
298
|
-
energyIntensity: { value:
|
|
294
|
+
energyIntensity: { value: 0.8 }, // Fixed value - no per-frame update needed
|
|
299
295
|
driftEnabled: { value: 1.0 },
|
|
300
296
|
driftSpeed: { value: 0.5 },
|
|
301
297
|
crossWaveEnabled: { value: 1.0 },
|
|
@@ -424,17 +420,15 @@ export class CrystalSoul {
|
|
|
424
420
|
uniforms.time.value += deltaTime / 1000;
|
|
425
421
|
}
|
|
426
422
|
|
|
427
|
-
// Update emotion color
|
|
423
|
+
// Update emotion color only if changed (avoid unnecessary GPU uniform sync)
|
|
428
424
|
if (uniforms.emotionColor && glowColor) {
|
|
429
|
-
uniforms.emotionColor.value
|
|
430
|
-
|
|
431
|
-
|
|
425
|
+
const current = uniforms.emotionColor.value;
|
|
426
|
+
if (current.r !== glowColor[0] || current.g !== glowColor[1] || current.b !== glowColor[2]) {
|
|
427
|
+
current.setRGB(glowColor[0], glowColor[1], glowColor[2]);
|
|
428
|
+
}
|
|
432
429
|
}
|
|
433
430
|
|
|
434
|
-
//
|
|
435
|
-
if (uniforms.energyIntensity) {
|
|
436
|
-
uniforms.energyIntensity.value = 0.8;
|
|
437
|
-
}
|
|
431
|
+
// Note: energyIntensity is fixed at 0.8 (set in constructor, no per-frame update needed)
|
|
438
432
|
|
|
439
433
|
// Apply breathing scale
|
|
440
434
|
if (this.mesh) {
|