Phase 3 update: orbit ghosts, parallax, FX, shader fix [build v8]

This commit is contained in:
2026-06-23 14:53:36 +10:00
parent 70dc130647
commit 802ae7fab9
3 changed files with 343 additions and 26 deletions
+31 -6
View File
@@ -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?.());
},
};