web: first-person sci-fi hands viewmodel (FAB Sci Fi Hands pack) — camera-parented rig with Idle/Walk/Run locomotion, 4 random punch clips on smash, Energy_Attack on record throw, Get draw-in on boot; merged 10-clip GLB with rebound textures (3.2MB)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e588508436
commit
c2fad7eb53
BIN
web/assets/hands.glb
Normal file
BIN
web/assets/hands.glb
Normal file
Binary file not shown.
73
web/hands.js
Normal file
73
web/hands.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// hands.js — first-person sci-fi hands (FAB "Sci Fi Hands" pack, merged to one GLB
|
||||||
|
// with clips: Idle, Idle_Fight, Attack_01-04, Energy_Attack_01, Get, Walk_01, Run_01).
|
||||||
|
// Camera-parented viewmodel: idle/walk bob from the pack's own clips, punch on smash.
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||||
|
|
||||||
|
const TUNE = {
|
||||||
|
pos: new THREE.Vector3(0, -1.55, 0.25),
|
||||||
|
rotY: Math.PI, // pack forward is +Z; camera looks down -Z
|
||||||
|
scale: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ATTACKS = ['Attack_01', 'Attack_02', 'Attack_03', 'Attack_04'];
|
||||||
|
|
||||||
|
export async function createHands(camera) {
|
||||||
|
const gltf = await new GLTFLoader().loadAsync('assets/hands.glb');
|
||||||
|
const rig = gltf.scene;
|
||||||
|
rig.position.copy(TUNE.pos);
|
||||||
|
rig.rotation.y = TUNE.rotY;
|
||||||
|
rig.scale.setScalar(TUNE.scale);
|
||||||
|
// render on top-ish: keep depth test but bump renderOrder so close geometry
|
||||||
|
// rarely swallows the arms; frustum culling off (always in view).
|
||||||
|
rig.traverse(o => { if (o.isMesh) { o.frustumCulled = false; o.renderOrder = 5; } });
|
||||||
|
camera.add(rig);
|
||||||
|
|
||||||
|
const mixer = new THREE.AnimationMixer(rig);
|
||||||
|
const clips = {};
|
||||||
|
for (const c of gltf.animations) clips[c.name] = mixer.clipAction(c);
|
||||||
|
const has = n => Object.prototype.hasOwnProperty.call(clips, n);
|
||||||
|
|
||||||
|
let current = null;
|
||||||
|
let attacking = false;
|
||||||
|
function play(name, { once = false, fade = 0.12 } = {}) {
|
||||||
|
if (!has(name)) return null;
|
||||||
|
const a = clips[name];
|
||||||
|
if (current === a && !once) return a;
|
||||||
|
if (current) current.fadeOut(fade);
|
||||||
|
a.reset().fadeIn(fade);
|
||||||
|
if (once) { a.setLoop(THREE.LoopOnce); a.clampWhenFinished = true; }
|
||||||
|
a.play();
|
||||||
|
current = a;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
mixer.addEventListener('finished', () => {
|
||||||
|
attacking = false;
|
||||||
|
play(lastLocomotion, { fade: 0.15 });
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastLocomotion = 'Idle';
|
||||||
|
// draw the hands on boot if the pack has a Get clip
|
||||||
|
if (has('Get')) { attacking = true; play('Get', { once: true }); }
|
||||||
|
else play('Idle');
|
||||||
|
|
||||||
|
return {
|
||||||
|
rig,
|
||||||
|
tune: TUNE,
|
||||||
|
update(dt, moving = false, running = false) {
|
||||||
|
const want = running ? 'Run_01' : moving ? 'Walk_01' : 'Idle';
|
||||||
|
lastLocomotion = has(want) ? want : 'Idle';
|
||||||
|
if (!attacking) play(lastLocomotion, { fade: 0.18 });
|
||||||
|
mixer.update(dt);
|
||||||
|
},
|
||||||
|
attack() {
|
||||||
|
attacking = true;
|
||||||
|
play(ATTACKS[Math.floor(Math.random() * ATTACKS.length)], { once: true, fade: 0.05 });
|
||||||
|
},
|
||||||
|
special() {
|
||||||
|
attacking = true;
|
||||||
|
play('Energy_Attack_01', { once: true, fade: 0.05 });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
12
web/main.js
12
web/main.js
@ -10,6 +10,7 @@ import { PointerLockControls } from 'three/addons/controls/PointerLockControls.j
|
|||||||
import { initPhysics, createPhysics } from './physics.js';
|
import { initPhysics, createPhysics } from './physics.js';
|
||||||
import { loadProp, loadFractured, fetchShelf, CATALOG } from './props.js';
|
import { loadProp, loadFractured, fetchShelf, CATALOG } from './props.js';
|
||||||
import { createJuice, MATERIALS } from './juice.js';
|
import { createJuice, MATERIALS } from './juice.js';
|
||||||
|
import { createHands } from './hands.js';
|
||||||
|
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const boot = $('boot');
|
const boot = $('boot');
|
||||||
@ -323,8 +324,9 @@ function smashProp(prop, power = 1.1) {
|
|||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
if (paused || !booted) return;
|
if (paused || !booted) return;
|
||||||
if (held) { throwHeld(); return; }
|
if (held) { hands?.special(); throwHeld(); return; }
|
||||||
if (lookRecord) { pullRecord(nearestRecord()); return; }
|
if (lookRecord) { pullRecord(nearestRecord()); return; }
|
||||||
|
hands?.attack();
|
||||||
if (lookProp) { smashProp(lookProp, 1.15); return; }
|
if (lookProp) { smashProp(lookProp, 1.15); return; }
|
||||||
}
|
}
|
||||||
// pointer-lock mode: a click is a smash. In drag-look mode the drag handlers own the
|
// pointer-lock mode: a click is a smash. In drag-look mode the drag handlers own the
|
||||||
@ -536,6 +538,8 @@ function w2() { return ROOM.w / 2; }
|
|||||||
function d2() { return ROOM.d / 2; }
|
function d2() { return ROOM.d / 2; }
|
||||||
|
|
||||||
// ---------------- main loop ----------------
|
// ---------------- main loop ----------------
|
||||||
|
let hands = null;
|
||||||
|
createHands(camera).then(h => { hands = h; }).catch(e => console.warn('hands failed', e));
|
||||||
const clock = new THREE.Clock();
|
const clock = new THREE.Clock();
|
||||||
let frame = 0;
|
let frame = 0;
|
||||||
|
|
||||||
@ -546,6 +550,10 @@ function stepFrame(dt) {
|
|||||||
camera.position.sub(lastJitter);
|
camera.position.sub(lastJitter);
|
||||||
|
|
||||||
if (!paused && !shareOpen) move(dt);
|
if (!paused && !shareOpen) move(dt);
|
||||||
|
if (hands) {
|
||||||
|
const moving = !paused && vel.lengthSq() > 0;
|
||||||
|
hands.update(dt, moving, moving && (keys.ShiftLeft || keys.ShiftRight));
|
||||||
|
}
|
||||||
|
|
||||||
updatePull(dt);
|
updatePull(dt);
|
||||||
if (++frame % 4 === 0) updateLook();
|
if (++frame % 4 === 0) updateLook();
|
||||||
@ -569,7 +577,7 @@ function tick() {
|
|||||||
// Pointer Lock (e.g. embedded frames / headless smoke tests) and step it by hand.
|
// Pointer Lock (e.g. embedded frames / headless smoke tests) and step it by hand.
|
||||||
if (location.search.includes('debug')) {
|
if (location.search.includes('debug')) {
|
||||||
window.DESTROY = {
|
window.DESTROY = {
|
||||||
get scene() { return scene; }, get camera() { return camera; }, get phys() { return phys; }, juice,
|
get scene() { return scene; }, get camera() { return camera; }, get phys() { return phys; }, get hands() { return hands; }, juice,
|
||||||
unpause() { paused = false; startEl.classList.add('hidden'); },
|
unpause() { paused = false; startEl.classList.add('hidden'); },
|
||||||
look(yaw, pitch = 0) { camera.rotation.set(pitch, yaw, 0, 'YXZ'); },
|
look(yaw, pitch = 0) { camera.rotation.set(pitch, yaw, 0, 'YXZ'); },
|
||||||
teleport(x, y, z) { camera.position.set(x, y, z); lastJitter.set(0, 0, 0); },
|
teleport(x, y, z) { camera.position.set(x, y, z); lastJitter.set(0, 0, 0); },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user