This commit is contained in:
2026-01-28 23:51:28 +01:00
parent 217a8581ae
commit f901481d6a
9 changed files with 522 additions and 132 deletions

BIN
assets/test_character.glb Normal file

Binary file not shown.

View File

@@ -11,7 +11,11 @@
<!-- Debug UI --> <!-- Debug UI -->
<div id="debug-ui"> <div id="debug-ui">
<div class="debug-header">
<h3>Debug Controls</h3> <h3>Debug Controls</h3>
<button id="minimize-btn"></button>
</div>
<div id="debug-content">
<div class="debug-item"> <div class="debug-item">
<label> <label>
<input type="checkbox" id="orbit-controls-toggle"> <input type="checkbox" id="orbit-controls-toggle">
@@ -25,14 +29,35 @@
</label> </label>
</div> </div>
<div class="debug-item"> <div class="debug-item">
<label for="file-input" class="file-label">Load GLB Model:</label> <label>
<input type="file" id="file-input" accept=".glb,.gltf"> <input type="checkbox" id="show-skeleton-toggle">
Show Skeleton
</label>
</div>
<div class="debug-item">
<label for="animation-select" class="asset-label">Select Animation:</label>
<select id="animation-select">
<option value="">-- No Animation --</option>
</select>
</div>
<div class="debug-item">
<label>
<input type="checkbox" id="play-animation-toggle">
Play Animation
</label>
</div>
<div class="debug-item">
<label for="asset-select" class="asset-label">Select Model:</label>
<select id="asset-select">
<option value="">-- No Model --</option>
</select>
</div> </div>
<div class="debug-item"> <div class="debug-item">
<span class="debug-label">Model Status:</span> <span class="debug-label">Model Status:</span>
<span id="model-status">No model loaded</span> <span id="model-status">No model loaded</span>
</div> </div>
</div> </div>
</div>
<!-- Three.js CDN --> <!-- Three.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
@@ -43,6 +68,15 @@
<!-- GLTFLoader --> <!-- GLTFLoader -->
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
<!-- Manager Classes -->
<script src="src/sceneManager.js"></script>
<script src="src/cameraManager.js"></script>
<script src="src/rendererManager.js"></script>
<script src="src/controlsManager.js"></script>
<!-- Debug UI Controller -->
<script src="src/debugUI.js"></script>
<!-- Your main script --> <!-- Your main script -->
<script src="src/main.js"></script> <script src="src/main.js"></script>
</body> </body>

26
src/cameraManager.js Normal file
View File

@@ -0,0 +1,26 @@
// Camera Setup
class CameraManager {
constructor() {
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.set(5, 5, 5);
this.camera.lookAt(0, 0, 0);
this.setupResizeHandler();
}
setupResizeHandler() {
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
});
}
getCamera() {
return this.camera;
}
}

19
src/controlsManager.js Normal file
View File

@@ -0,0 +1,19 @@
// Orbit Controls Setup
class ControlsManager {
constructor(camera, renderer) {
this.controls = new THREE.OrbitControls(camera, renderer.domElement);
this.controls.enabled = false;
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
}
getControls() {
return this.controls;
}
update() {
if (this.controls.enabled) {
this.controls.update();
}
}
}

279
src/debugUI.js Normal file
View File

@@ -0,0 +1,279 @@
// Debug UI Controller
class DebugUI {
constructor(scene, camera, controls, gridHelper, loader) {
this.scene = scene;
this.camera = camera;
this.controls = controls;
this.gridHelper = gridHelper;
this.loader = loader;
this.loadedModel = null;
this.skeletonHelper = null;
this.animationMixer = null;
this.animations = [];
this.currentAction = null;
this.isMinimized = false;
this.init();
}
init() {
this.setupMinimize();
this.setupOrbitControls();
this.setupGridToggle();
this.setupSkeletonToggle();
this.setupAnimationControls();
this.setupAssetSelector();
}
setupMinimize() {
const debugUI = document.getElementById('debug-ui');
const minimizeBtn = document.getElementById('minimize-btn');
minimizeBtn.addEventListener('click', () => {
this.isMinimized = !this.isMinimized;
debugUI.classList.toggle('minimized', this.isMinimized);
minimizeBtn.textContent = this.isMinimized ? '+' : '';
});
}
setupOrbitControls() {
document.getElementById('orbit-controls-toggle').addEventListener('change', (event) => {
this.controls.enabled = event.target.checked;
});
}
setupGridToggle() {
document.getElementById('show-grid-toggle').addEventListener('change', (event) => {
this.gridHelper.visible = event.target.checked;
});
}
setupSkeletonToggle() {
document.getElementById('show-skeleton-toggle').addEventListener('change', (event) => {
if (event.target.checked) {
this.showSkeleton();
} else {
this.hideSkeleton();
}
});
}
showSkeleton() {
if (!this.loadedModel) return;
this.loadedModel.traverse((child) => {
if (child.isSkinnedMesh && child.skeleton) {
if (!this.skeletonHelper) {
this.skeletonHelper = new THREE.SkeletonHelper(this.loadedModel);
this.skeletonHelper.material.linewidth = 2;
this.scene.add(this.skeletonHelper);
}
}
});
}
hideSkeleton() {
if (this.skeletonHelper) {
this.scene.remove(this.skeletonHelper);
this.skeletonHelper = null;
}
}
setupAnimationControls() {
// Animation selector
document.getElementById('animation-select').addEventListener('change', (event) => {
this.selectAnimation(event.target.value);
});
// Play animation toggle
document.getElementById('play-animation-toggle').addEventListener('change', (event) => {
if (event.target.checked) {
this.playAnimation();
} else {
this.pauseAnimation();
}
});
}
selectAnimation(animationName) {
if (!this.animationMixer || !animationName) {
this.pauseAnimation();
return;
}
// Stop current action
if (this.currentAction) {
this.currentAction.stop();
}
// Find and play selected animation
const clip = this.animations.find(anim => anim.name === animationName);
if (clip) {
this.currentAction = this.animationMixer.clipAction(clip);
// Auto-play if checkbox is checked
if (document.getElementById('play-animation-toggle').checked) {
this.currentAction.play();
}
}
}
playAnimation() {
if (this.currentAction) {
this.currentAction.play();
}
}
pauseAnimation() {
if (this.currentAction) {
this.currentAction.paused = true;
}
}
updateAnimationMixer(delta) {
if (this.animationMixer) {
this.animationMixer.update(delta);
}
}
// Program custom animations here
createCustomAnimations() {
// Example: Create a custom rotation animation
const times = [0, 2];
const values = [0, Math.PI*2];
const track = new THREE.NumberKeyframeTrack('.rotation[y]', times, values);
const clip = new THREE.AnimationClip('RotateY', 2, [track]);
return [clip];
return []; // Return array of custom AnimationClip objects
}
setupAssetSelector() {
// Available assets in the assets folder
const assets = [
'assets/test_character.glb'
// Add more assets here as needed
];
// Populate asset dropdown
const assetSelect = document.getElementById('asset-select');
assets.forEach(assetPath => {
const option = document.createElement('option');
option.value = assetPath;
option.textContent = assetPath.split('/').pop(); // Show only filename
assetSelect.appendChild(option);
});
// Asset selection handler
assetSelect.addEventListener('change', (event) => {
this.loadAsset(event.target.value);
});
}
loadAsset(assetPath) {
if (!assetPath) {
// Remove model if "No Model" is selected
if (this.loadedModel) {
this.scene.remove(this.loadedModel);
this.loadedModel = null;
}
this.updateModelStatus('No model loaded');
return;
}
// Load selected asset
this.updateModelStatus('Loading...');
this.loader.load(
assetPath,
(gltf) => {
// Remove previous model if exists
if (this.loadedModel) {
this.scene.remove(this.loadedModel);
}
// Remove previous skeleton helper if exists
this.hideSkeleton();
// Uncheck skeleton toggle
document.getElementById('show-skeleton-toggle').checked = false;
this.loadedModel = gltf.scene;
// Get bounding box
const box = new THREE.Box3().setFromObject(this.loadedModel);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
// Calculate max dimension
const maxDim = Math.max(size.x, size.y, size.z);
// Scale model to fit in a 4 unit cube (fits nicely with the grid)
const targetSize = 4;
const scale = targetSize / maxDim;
this.loadedModel.scale.setScalar(scale);
// Recalculate box after scaling
box.setFromObject(this.loadedModel);
box.getCenter(center);
// Center the model at origin on X and Z, but place bottom at y=0
this.loadedModel.position.x = -center.x;
this.loadedModel.position.y = -box.min.y;
this.loadedModel.position.z = -center.z;
this.scene.add(this.loadedModel);
// Setup animations
this.animations = gltf.animations || [];
// Add custom animations
const customAnimations = this.createCustomAnimations();
this.animations = [...this.animations, ...customAnimations];
if (this.animations.length > 0) {
this.animationMixer = new THREE.AnimationMixer(this.loadedModel);
// Populate animation dropdown
const animSelect = document.getElementById('animation-select');
animSelect.innerHTML = '<option value="">-- No Animation --</option>';
this.animations.forEach(clip => {
const option = document.createElement('option');
option.value = clip.name;
option.textContent = clip.name;
animSelect.appendChild(option);
});
} else {
this.animationMixer = null;
document.getElementById('animation-select').innerHTML = '<option value="">-- No Animation --</option>';
}
// Reset animation controls
document.getElementById('animation-select').value = '';
document.getElementById('play-animation-toggle').checked = false;
// Check for skeleton
let hasSkeleton = false;
this.loadedModel.traverse((child) => {
if (child.isSkinnedMesh) {
hasSkeleton = true;
console.log('Skeleton found:', child.skeleton);
}
});
const statusText = hasSkeleton ? 'Model loaded & fitted (with skeleton)' : 'Model loaded & fitted';
this.updateModelStatus(statusText);
},
undefined,
(error) => {
console.error('Error loading model:', error);
this.updateModelStatus('Error loading model');
}
);
}
updateModelStatus(status) {
document.getElementById('model-status').textContent = status;
}
}

View File

@@ -1,125 +1,42 @@
// Scene setup // Initialize all managers
const scene = new THREE.Scene(); const sceneManager = new SceneManager();
scene.background = new THREE.Color(0xffffff); const cameraManager = new CameraManager();
const rendererManager = new RendererManager();
// Camera setup const controlsManager = new ControlsManager(
const camera = new THREE.PerspectiveCamera( cameraManager.getCamera(),
75, rendererManager.getRenderer()
window.innerWidth / window.innerHeight,
0.1,
1000
); );
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);
// Renderer setup // Get instances
const renderer = new THREE.WebGLRenderer({ antialias: true }); const scene = sceneManager.getScene();
renderer.setSize(window.innerWidth, window.innerHeight); const camera = cameraManager.getCamera();
renderer.setPixelRatio(window.devicePixelRatio); const renderer = rendererManager.getRenderer();
document.getElementById('canvas-container').appendChild(renderer.domElement); const controls = controlsManager.getControls();
const gridHelper = sceneManager.getGridHelper();
// Orbit Controls (initially disabled)
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enabled = false;
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 5);
scene.add(directionalLight);
// Grid
const gridHelper = new THREE.GridHelper(20, 20, 0x000000, 0x888888);
scene.add(gridHelper);
// Model container
let loadedModel = null;
// GLB Loader // GLB Loader
const loader = new THREE.GLTFLoader(); const loader = new THREE.GLTFLoader();
// File input handler // Initialize Debug UI
document.getElementById('file-input').addEventListener('change', (event) => { const debugUI = new DebugUI(scene, camera, controls, gridHelper, loader);
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const arrayBuffer = e.target.result;
// Remove previous model if exists
if (loadedModel) {
scene.remove(loadedModel);
}
// Load the GLB
loader.parse(arrayBuffer, '', (gltf) => {
loadedModel = gltf.scene;
// Get bounding box
const box = new THREE.Box3().setFromObject(loadedModel);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
// Calculate max dimension
const maxDim = Math.max(size.x, size.y, size.z);
// Scale model to fit in a 4 unit cube (fits nicely with the grid)
const targetSize = 4;
const scale = targetSize / maxDim;
loadedModel.scale.setScalar(scale);
// Recalculate box after scaling
box.setFromObject(loadedModel);
box.getCenter(center);
// Center the model at origin on X and Z, but place bottom at y=0
loadedModel.position.x = -center.x;
loadedModel.position.y = -box.min.y;
loadedModel.position.z = -center.z;
scene.add(loadedModel);
document.getElementById('model-status').textContent = 'Model loaded & fitted';
}, (error) => {
console.error('Error loading model:', error);
document.getElementById('model-status').textContent = 'Error loading model';
});
};
reader.readAsArrayBuffer(file);
document.getElementById('model-status').textContent = 'Loading...';
}
});
// Orbit controls toggle
document.getElementById('orbit-controls-toggle').addEventListener('change', (event) => {
controls.enabled = event.target.checked;
});
// Grid toggle
document.getElementById('show-grid-toggle').addEventListener('change', (event) => {
gridHelper.visible = event.target.checked;
});
// Animation loop // Animation loop
const clock = new THREE.Clock();
function animate() { function animate() {
requestAnimationFrame(animate); requestAnimationFrame(animate);
if (controls.enabled) { const delta = clock.getDelta();
controls.update();
}
renderer.render(scene, camera); // Update controls
controlsManager.update();
// Update animations
debugUI.updateAnimationMixer(delta);
// Render scene
rendererManager.render(scene, camera);
} }
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start animation // Start animation
animate(); animate();

25
src/rendererManager.js Normal file
View File

@@ -0,0 +1,25 @@
// Renderer Setup
class RendererManager {
constructor() {
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('canvas-container').appendChild(this.renderer.domElement);
this.setupResizeHandler();
}
setupResizeHandler() {
window.addEventListener('resize', () => {
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
}
getRenderer() {
return this.renderer;
}
render(scene, camera) {
this.renderer.render(scene, camera);
}
}

32
src/sceneManager.js Normal file
View File

@@ -0,0 +1,32 @@
// Scene Setup
class SceneManager {
constructor() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xffffff);
this.setupLighting();
this.setupGrid();
}
setupLighting() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 5);
this.scene.add(directionalLight);
}
setupGrid() {
this.gridHelper = new THREE.GridHelper(20, 20, 0x000000, 0x888888);
this.scene.add(this.gridHelper);
}
getScene() {
return this.scene;
}
getGridHelper() {
return this.gridHelper;
}
}

View File

@@ -38,10 +38,56 @@ canvas {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: black; color: black;
border-bottom: 2px solid #333;
padding-bottom: 8px; padding-bottom: 8px;
} }
.debug-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
border-bottom: 2px solid #333;
padding-bottom: 0px;
}
.debug-header h3 {
margin: 0;
border: none;
padding: 0;
}
#minimize-btn {
background: #333;
color: white;
border: none;
border-radius: 10px;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 18px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
#minimize-btn:hover {
background: #555;
}
#debug-content {
transition: max-height 0.3s ease, opacity 0.3s ease;
max-height: 500px;
opacity: 1;
overflow: hidden;
}
#debug-ui.minimized #debug-content {
max-height: 0;
opacity: 0;
}
.debug-item { .debug-item {
margin-bottom: 12px; margin-bottom: 12px;
} }
@@ -73,7 +119,19 @@ canvas {
border-radius: 4px; border-radius: 4px;
} }
.file-label { .debug-item select {
width: 100%;
padding: 6px;
margin-top: 5px;
font-size: 13px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
}
.file-label,
.asset-label {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
font-weight: bold; font-weight: bold;