Created
March 16, 2025 16:23
-
-
Save lukevanin/3d049265b16b98096ed55740cce0ae02 to your computer and use it in GitHub Desktop.
Vibe coded 3D engine utilities
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import Three.js and additional modules | |
import * as THREE from 'three'; | |
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'; | |
import { AnimationClip, AnimationMixer, Object3D, Scene, Vector3 } from 'three'; | |
import { AnimationDebugInfo } from './debug'; | |
// Global animation speed multiplier | |
const ANIMATION_SPEED_MULTIPLIER = 1.2; | |
// Animation variables | |
let mixer: THREE.AnimationMixer | null = null; | |
let character: THREE.Object3D | null = null; | |
// Function to remove root motion (position) from animation | |
export function removeRootMotion(animation: AnimationClip): void { | |
// Find the root bone track (usually the first track with position) | |
const tracks = animation.tracks; | |
// Iterate through tracks to find position tracks | |
for (let i = 0; i < tracks.length; i++) { | |
const track = tracks[i]; | |
// Check if it's a position track for the root bone | |
// Position tracks typically end with ".position" | |
if (track.name.endsWith('.position')) { | |
// This looks like a root bone position track | |
console.log(`Removing root motion from track: ${track.name}`); | |
// Get the values array (contains x,y,z values for each keyframe) | |
const values = track.values; | |
// Get the initial position from the first frame | |
const initialZ = values[2]; | |
// Set all keyframes to keep initial Z position only | |
for (let j = 0; j < values.length; j += 3) { | |
// Keep original X,Y values | |
values[j + 2] = initialZ; // Lock Z position | |
} | |
} | |
} | |
} | |
// Update animation mixer | |
function updateAnimations(deltaTime: number): void { | |
if (mixer) { | |
mixer.update(deltaTime); | |
} | |
} | |
// Function to get debug information about the mixer | |
export function getAnimationDebugInfo(): AnimationDebugInfo { | |
return { | |
mixerExists: mixer !== null, | |
animationCount: 0, | |
currentAction: null, | |
availableAnimations: [] | |
}; | |
} | |
// Function to load a model for an actor | |
export async function loadActorModel( | |
modelPath: string, | |
scene: Scene, | |
position: Vector3, | |
scale: number = 1.0 // Default scale is 1.0 | |
): Promise<{ model: Object3D, mixer: AnimationMixer }> { | |
return new Promise<{ model: Object3D, mixer: AnimationMixer }>((resolve, reject) => { | |
const fbxLoader = new FBXLoader(); | |
// Determine if the path already includes a file extension | |
const hasExtension = modelPath.toLowerCase().endsWith('.fbx'); | |
const fullPath = hasExtension ? modelPath : `${modelPath}/model.fbx`; | |
console.log(`Loading model from path: ${fullPath}`, position, scale); | |
// Load character model | |
fbxLoader.load( | |
fullPath, | |
(fbx) => { | |
const model = fbx; | |
console.log(`Model loaded successfully: ${model.name}`); | |
// Scale and position the model | |
model.scale.setScalar(scale); // Use the provided scale parameter | |
model.position.copy(position); | |
// Enable shadows for all meshes in the model | |
model.traverse((child) => { | |
if ((child as THREE.Mesh).isMesh) { | |
const mesh = child as THREE.Mesh; | |
mesh.castShadow = true; | |
mesh.receiveShadow = false; | |
// Recompute normals for the mesh geometry | |
if (mesh.geometry) { | |
// Make sure we're working with a BufferGeometry | |
if (mesh.geometry instanceof THREE.BufferGeometry) { | |
// Check if the geometry has position attribute | |
if (mesh.geometry.attributes.position) { | |
// Compute vertex normals | |
mesh.geometry.computeVertexNormals(); | |
console.log(`Recomputed normals for mesh: ${mesh.name || 'unnamed'}`); | |
} | |
} | |
} | |
} | |
}); | |
// Add to scene | |
scene.add(model); | |
// Create animation mixer | |
const newMixer = new AnimationMixer(model); | |
// Set global references | |
character = model; | |
mixer = newMixer; | |
// Resolve with both the model and mixer | |
resolve({ | |
model: model, | |
mixer: newMixer | |
}); | |
}, | |
(xhr) => { | |
console.log((xhr.loaded / xhr.total) * 100 + '% model loaded'); | |
}, | |
(error) => { | |
console.error('Error loading model:', error); | |
reject(error); | |
} | |
); | |
}); | |
} | |
// Get character object | |
function getCharacter(): Object3D | null { | |
return character; | |
} | |
// Get mixer object | |
function getMixer(): AnimationMixer | null { | |
return mixer; | |
} | |
// Export functions | |
export { | |
updateAnimations, | |
getCharacter, | |
getMixer | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment