diff --git a/public/ghost-visual.js b/public/ghost-visual.js index eae3e00..bc49643 100644 --- a/public/ghost-visual.js +++ b/public/ghost-visual.js @@ -65,9 +65,18 @@ function hexToRGB(THREE, hex) { return new THREE.Color(hex); } * Kept cheap: a subdivided cone/teardrop so the gradient + billow read well. */ function buildWispGeometry(THREE) { - // teardrop: radius shrinks toward the tail (top in UV space = head) - const geo = new THREE.CylinderGeometry(0.0, 0.5, 1.2, 18, 8, true); - geo.translate(0, 0.1, 0); + // teardrop-ish body: rounded head (sphere-like top) tapering to a wispy tail. + // Lathe a profile curve so it reads as a ghost rather than a hard cone. + const pts = []; + const N = 12; + for (let i = 0; i <= N; i++) { + const t = i / N; // 0 = tail bottom, 1 = head top + const y = -0.6 + t * 1.2; // height -0.6 .. 0.6 + // radius: thin pointed tail, bulging rounded head + const r = Math.sin(t * Math.PI * 0.92) * 0.42 + t * 0.06; + pts.push(new THREE.Vector2(Math.max(0.001, r), y)); + } + const geo = new THREE.LatheGeometry(pts, 20); return geo; } @@ -98,6 +107,18 @@ export function createGhostVisual(THREE, opts = {}) { const body = new THREE.Mesh(buildWispGeometry(THREE), mat); group.add(body); + // --- always-on soft glow halo (procedural, no texture needed) so the ghost + // never looks like a bare cone even before FX textures attach --- + const haloMat = new THREE.SpriteMaterial({ + color: hexToRGB(THREE, grad.bottom), + transparent: true, opacity: 0.35, depthWrite: false, + blending: THREE.AdditiveBlending, + }); + const halo = new THREE.Sprite(haloMat); + halo.scale.setScalar(1.5); + halo.position.y = 0.1; + group.add(halo); + // --- Tier 1: FX billboards (progressive enhancement) --- const fxSprites = []; function addFxBillboard(texture, scale, yOffset, opacity, blend) { @@ -121,8 +142,10 @@ export function createGhostVisual(THREE, opts = {}) { let fxAttached = false; function attachFx(textures) { if (!wantFx || fxAttached || !textures) return; - if (textures.glow) addFxBillboard(textures.glow, 1.6, 0.1, 0.5); - if (textures.trail) addFxBillboard(textures.trail, 1.2, -0.4, 0.35); + // give the procedural halo a real soft-glow texture if provided + if (textures.glow) { haloMat.map = textures.glow; haloMat.opacity = 0.55; haloMat.needsUpdate = true; } + if (textures.trail) addFxBillboard(textures.trail, 1.0, -0.45, 0.4); + if (textures.smoke) addFxBillboard(textures.smoke, 0.8, 0.3, 0.25); fxAttached = true; } @@ -150,6 +173,7 @@ export function createGhostVisual(THREE, opts = {}) { // stays fixed (previously this accumulated onto group.y and the ghost // drifted up out of view every frame). body.position.y = Math.sin(elapsed * 1.8) * 0.08; + halo.position.y = 0.1 + Math.sin(elapsed * 1.8) * 0.08; // halo bobs with body // billboard FX face camera handled by Sprite automatically if (state === 'spawn') { const k = Math.min(stateT / 0.6, 1); @@ -178,10 +202,11 @@ export function createGhostVisual(THREE, opts = {}) { const g2 = GHOST_GRADIENTS[next]; if (!g2) return; uniforms.uTop.value.set(g2.top); uniforms.uBottom.value.set(g2.bottom); + haloMat.color.set(g2.bottom); fxSprites.forEach(s => s.material.color.set(g2.top)); }, dispose() { - body.geometry?.dispose?.(); mat.dispose?.(); + body.geometry?.dispose?.(); mat.dispose?.(); haloMat.dispose?.(); fxSprites.forEach(s => s.material?.map?.dispose?.()); }, }; diff --git a/public/hunt.html b/public/hunt.html index a2e6c88..a42038f 100644 --- a/public/hunt.html +++ b/public/hunt.html @@ -3,7 +3,7 @@
-