update
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
/* Newbury Exhibit — LEGO-head ghost (3D)
|
||||
*
|
||||
* A classic minifig-head SILHOUETTE (cylinder body, domed top, stud) given a
|
||||
* translucent ghost treatment in a Hidden Side gradient. Built from primitives
|
||||
* so there's no external model file to load. Original eyes + wispy tail — we
|
||||
* deliberately do NOT reproduce any printed LEGO face or licensed character.
|
||||
*
|
||||
* Exports buildLegoHeadGhost(THREE, colorKey) -> THREE.Group (units: mm).
|
||||
* The group's local origin is at the centre of the head so it sits nicely on a
|
||||
* marker anchor. Call group.userData.update(tSeconds) each frame for the idle
|
||||
* bob/sway/blink animation.
|
||||
*/
|
||||
|
||||
// Hidden Side ghost-lure gradients (recovered GhostType data).
|
||||
export const GHOST_GRADIENTS = {
|
||||
Red: { top: 0xf65151, bottom: 0xff2678 },
|
||||
Yellow: { top: 0xb57f0b, bottom: 0xfff35d },
|
||||
Blue: { top: 0x529eff, bottom: 0x51eaf1 },
|
||||
};
|
||||
|
||||
// Vertical gradient + soft rim glow, applied to the head body.
|
||||
const GHOST_VERT = `
|
||||
varying vec3 vNormalW;
|
||||
varying vec3 vViewDir;
|
||||
varying float vY;
|
||||
uniform float uMinY;
|
||||
uniform float uMaxY;
|
||||
void main() {
|
||||
vec4 wp = modelMatrix * vec4(position, 1.0);
|
||||
vNormalW = normalize(mat3(modelMatrix) * normal);
|
||||
vViewDir = normalize(cameraPosition - wp.xyz);
|
||||
vY = clamp((position.y - uMinY) / (uMaxY - uMinY), 0.0, 1.0);
|
||||
gl_Position = projectionMatrix * viewMatrix * wp;
|
||||
}
|
||||
`;
|
||||
|
||||
const GHOST_FRAG = `
|
||||
varying vec3 vNormalW;
|
||||
varying vec3 vViewDir;
|
||||
varying float vY;
|
||||
uniform vec3 uTop;
|
||||
uniform vec3 uBottom;
|
||||
uniform float uOpacity;
|
||||
uniform float uTime;
|
||||
void main() {
|
||||
// body gradient (top lighter -> bottom saturated)
|
||||
vec3 base = mix(uBottom, uTop, vY);
|
||||
// fresnel rim so the silhouette edge glows like a spectre
|
||||
float fres = pow(1.0 - max(dot(normalize(vNormalW), normalize(vViewDir)), 0.0), 2.2);
|
||||
vec3 col = base + fres * vec3(0.55, 0.85, 0.95);
|
||||
// gentle internal shimmer
|
||||
float shimmer = 0.06 * sin(vY * 18.0 - uTime * 2.2);
|
||||
col += shimmer;
|
||||
float alpha = uOpacity * (0.55 + 0.45 * fres);
|
||||
gl_FragColor = vec4(col, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
function ghostBodyMaterial(THREE, colorKey, minY, maxY) {
|
||||
const g = GHOST_GRADIENTS[colorKey] || GHOST_GRADIENTS.Blue;
|
||||
return new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uTop: { value: new THREE.Color(g.bottom) }, // brighter accent at top
|
||||
uBottom: { value: new THREE.Color(g.top) }, // deeper hue at base
|
||||
uOpacity: { value: 0.82 },
|
||||
uMinY: { value: minY },
|
||||
uMaxY: { value: maxY },
|
||||
uTime: { value: 0 },
|
||||
},
|
||||
vertexShader: GHOST_VERT,
|
||||
fragmentShader: GHOST_FRAG,
|
||||
transparent: true,
|
||||
depthWrite: false, // translucent: don't occlude its own backfaces oddly
|
||||
side: THREE.DoubleSide,
|
||||
blending: THREE.NormalBlending,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildLegoHeadGhost(THREE, colorKey = 'Blue') {
|
||||
const group = new THREE.Group();
|
||||
|
||||
// --- dimensions in mm (classic minifig head proportions, ghost-scaled) ---
|
||||
const R = 22; // head radius
|
||||
const BODY_H = 46; // cylinder height
|
||||
const DOME_H = 16; // top dome rise
|
||||
const STUD_R = 9;
|
||||
const STUD_H = 9;
|
||||
|
||||
const minY = -BODY_H / 2 - 26; // include tail reach for gradient base
|
||||
const maxY = BODY_H / 2 + DOME_H + STUD_H;
|
||||
const bodyMat = ghostBodyMaterial(THREE, colorKey, minY, maxY);
|
||||
|
||||
const mats = [bodyMat]; // track shader mats for time uniform
|
||||
|
||||
// cylinder body
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(R, R, BODY_H, 40, 1, true),
|
||||
bodyMat
|
||||
);
|
||||
group.add(body);
|
||||
|
||||
// domed top — half-sphere squashed to DOME_H
|
||||
const dome = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(R, 40, 20, 0, Math.PI * 2, 0, Math.PI / 2),
|
||||
bodyMat
|
||||
);
|
||||
dome.scale.set(1, DOME_H / R, 1);
|
||||
dome.position.y = BODY_H / 2;
|
||||
group.add(dome);
|
||||
|
||||
// bottom cap (rounded) so the base reads solid
|
||||
const cap = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(R, 40, 16, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2),
|
||||
bodyMat
|
||||
);
|
||||
cap.scale.set(1, 0.35, 1);
|
||||
cap.position.y = -BODY_H / 2;
|
||||
group.add(cap);
|
||||
|
||||
// the stud on top — the unmistakable LEGO tell
|
||||
const stud = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(STUD_R, STUD_R, STUD_H, 28),
|
||||
bodyMat
|
||||
);
|
||||
stud.position.y = BODY_H / 2 + DOME_H + STUD_H / 2 - 2;
|
||||
group.add(stud);
|
||||
|
||||
// --- eyes (original, simple) — solid dark ovals on the front (+Z) face ---
|
||||
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x081026 });
|
||||
const glintMat = new THREE.MeshBasicMaterial({ color: 0xcfeeff });
|
||||
function eye(sign) {
|
||||
const e = new THREE.Group();
|
||||
const ball = new THREE.Mesh(new THREE.SphereGeometry(5.2, 20, 16), eyeMat);
|
||||
ball.scale.set(0.8, 1.15, 0.5);
|
||||
e.add(ball);
|
||||
const glint = new THREE.Mesh(new THREE.SphereGeometry(1.5, 10, 8), glintMat);
|
||||
glint.position.set(sign * -0.8, 2.0, 4.6);
|
||||
e.add(glint);
|
||||
e.position.set(sign * 9, 2, R - 2);
|
||||
return e;
|
||||
}
|
||||
const eyes = new THREE.Group();
|
||||
eyes.add(eye(-1), eye(1));
|
||||
group.add(eyes);
|
||||
|
||||
// --- wispy tail: a few translucent "drips" below the body ---
|
||||
const tail = new THREE.Group();
|
||||
const tailMat = bodyMat;
|
||||
const drips = 4;
|
||||
for (let i = 0; i < drips; i++) {
|
||||
const dr = R * (0.5 - i * 0.06);
|
||||
const drip = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(dr * 0.6, 22 + i * 2, 18, 1, true),
|
||||
tailMat
|
||||
);
|
||||
const ang = (i / drips) * Math.PI * 2;
|
||||
drip.position.set(Math.cos(ang) * R * 0.5, -BODY_H / 2 - 8, Math.sin(ang) * R * 0.5);
|
||||
drip.userData.phase = ang;
|
||||
tail.add(drip);
|
||||
}
|
||||
group.add(tail);
|
||||
|
||||
// --- idle animation ---
|
||||
let baseY = 0;
|
||||
group.userData.colorKey = colorKey;
|
||||
group.userData.materials = mats;
|
||||
group.userData.update = function (t) {
|
||||
for (const m of mats) m.uniforms.uTime.value = t;
|
||||
// bob + sway
|
||||
group.position.y = baseY + Math.sin(t * 1.8) * 6;
|
||||
eyes.rotation.y = Math.sin(t * 0.6) * 0.15;
|
||||
// tail drips waggle
|
||||
tail.children.forEach((d, i) => {
|
||||
d.position.y = -BODY_H / 2 - 8 + Math.sin(t * 3 + d.userData.phase) * 3;
|
||||
d.rotation.z = Math.sin(t * 2 + i) * 0.12;
|
||||
});
|
||||
// occasional blink (scale eyes flat briefly)
|
||||
const blink = (Math.sin(t * 0.9) > 0.985) ? 0.1 : 1.0;
|
||||
eyes.scale.y = eyes.scale.y + (blink - eyes.scale.y) * 0.4;
|
||||
};
|
||||
group.userData.setBaseY = (y) => { baseY = y; };
|
||||
|
||||
// recolour at runtime (e.g. when server assigns a ghost's lure colour)
|
||||
group.userData.setColor = function (key) {
|
||||
const g = GHOST_GRADIENTS[key] || GHOST_GRADIENTS.Blue;
|
||||
bodyMat.uniforms.uTop.value.set(g.bottom);
|
||||
bodyMat.uniforms.uBottom.value.set(g.top);
|
||||
group.userData.colorKey = key;
|
||||
};
|
||||
|
||||
return group;
|
||||
}
|
||||
Reference in New Issue
Block a user