diff --git a/src/camera-config.js b/src/camera-config.js new file mode 100644 index 0000000..26f15a8 --- /dev/null +++ b/src/camera-config.js @@ -0,0 +1,24 @@ +// Load camera configuration from config file +export async function loadCameraConfig() { + try { + const response = await fetch('./camera-config.json'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const cameraConfig = await response.json(); + console.log('โœ… Camera config loaded successfully:', cameraConfig); + return cameraConfig; + } catch (error) { + console.warn('โš ๏ธ Could not load camera-config.json, using original defaults.'); + console.log('๐Ÿ“ To use custom camera settings, ensure camera-config.json is accessible from your web server.'); + + // Return original simple defaults + return { + camera: { + position: { x: 20, y: 20, z: 20 }, + target: { x: 0, y: 0, z: 0 }, + distance: 34.64 + } + }; + } +} diff --git a/src/fpv-controls.js b/src/fpv-controls.js new file mode 100644 index 0000000..034da61 --- /dev/null +++ b/src/fpv-controls.js @@ -0,0 +1,328 @@ +import * as THREE from 'three'; + +// FPV Mode variables +let fpvMode = false; +let fpvControls = { + moveForward: false, + moveBackward: false, + moveLeft: false, + moveRight: false, + jump: false, + crouch: false, + canJump: true, + velocity: new THREE.Vector3(), + direction: new THREE.Vector3(), + rotation: { x: 0, y: 0 }, + verticalVelocity: 0, + isJumping: false, + isCrouching: false, + currentHeight: 0, + targetHeight: 0 +}; + +export function setupFPVButton(renderer) { + const fpvBtn = document.getElementById('fpv-btn'); + if (!fpvBtn) { + console.warn('FPV button not found'); + return; + } + + fpvBtn.addEventListener('click', () => { + if (!fpvMode) { + enterFPVMode(renderer); + } else { + exitFPVMode(); + } + }); + + console.log('๐Ÿšถ FPV button set up'); +} + +export function setupFPVControls() { + // Keyboard event handlers + document.addEventListener('keydown', onKeyDown); + document.addEventListener('keyup', onKeyUp); + + // Mouse event handlers for FPV mode + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('click', onMouseClick); + + console.log('FPV controls setup complete. Press F to enter FPV mode'); +} + +function onKeyDown(event) { + switch (event.code) { + case 'KeyF': + event.preventDefault(); + event.stopPropagation(); + if (!fpvMode) { + enterFPVMode(); + } else { + exitFPVMode(); + } + return false; + case 'Escape': + if (fpvMode) { + event.preventDefault(); + event.stopPropagation(); + exitFPVMode(); + return false; + } + break; + case 'KeyW': + if (fpvMode) fpvControls.moveForward = true; + break; + case 'KeyA': + if (fpvMode) fpvControls.moveLeft = true; + break; + case 'KeyS': + if (fpvMode) fpvControls.moveBackward = true; + break; + case 'KeyD': + if (fpvMode) fpvControls.moveRight = true; + break; + case 'Space': + if (fpvMode) { + event.preventDefault(); + fpvControls.jump = true; + } + break; + case 'ShiftLeft': + case 'ShiftRight': + if (fpvMode) { + event.preventDefault(); + fpvControls.crouch = true; + fpvControls.isCrouching = true; + } + break; + } +} + +function onKeyUp(event) { + switch (event.code) { + case 'KeyW': + if (fpvMode) fpvControls.moveForward = false; + break; + case 'KeyA': + if (fpvMode) fpvControls.moveLeft = false; + break; + case 'KeyS': + if (fpvMode) fpvControls.moveBackward = false; + break; + case 'KeyD': + if (fpvMode) fpvControls.moveRight = false; + break; + case 'Space': + if (fpvMode) fpvControls.jump = false; + break; + case 'ShiftLeft': + case 'ShiftRight': + if (fpvMode) { + fpvControls.crouch = false; + fpvControls.isCrouching = false; + } + break; + } +} + +function onMouseMove(event) { + if (!fpvMode) return; + + const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; + const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; + + fpvControls.rotation.y -= movementX * 0.002; // Horizontal rotation (yaw) + fpvControls.rotation.x -= movementY * 0.002; // Vertical rotation (pitch) + + // Limit vertical look angle + fpvControls.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, fpvControls.rotation.x)); +} + +function onMouseClick(event) { + if (fpvMode && event.target && event.target.requestPointerLock) { + // Request pointer lock for mouse control + event.target.requestPointerLock(); + } +} + +function enterFPVMode(renderer) { + if (!window.modelBounds) { + console.warn('Model not loaded yet, cannot enter FPV mode'); + return; + } + + console.log('๐Ÿšถ Entering FPV mode'); + fpvMode = true; + + // Disable orbit controls + if (window.controls) { + window.controls.enabled = false; + } + + // Keep current camera position instead of forcing to model center + // Just ensure the height is reasonable for walking + fpvControls.currentHeight = Math.max(window.camera.position.y, window.eyeHeight); + fpvControls.targetHeight = fpvControls.currentHeight; + window.camera.position.y = fpvControls.currentHeight; + + // Calculate rotation based on current camera orientation + const euler = new THREE.Euler().setFromQuaternion(window.camera.quaternion, 'YXZ'); + fpvControls.rotation.x = euler.x; + fpvControls.rotation.y = euler.y; + + // Reset movement state + fpvControls.velocity.set(0, 0, 0); + fpvControls.verticalVelocity = 0; + fpvControls.isJumping = false; + fpvControls.isCrouching = false; + + // Show crosshair + const crosshair = document.getElementById('fpv-crosshair'); + if (crosshair) { + crosshair.style.display = 'block'; + } + + // Update button text + const fpvBtn = document.getElementById('fpv-btn'); + if (fpvBtn) { + fpvBtn.textContent = 'Exit FPV Mode'; + fpvBtn.style.background = '#FF5722'; + } + + // Request pointer lock + if (renderer && renderer.domElement) { + renderer.domElement.requestPointerLock(); + } + + console.log('FPV mode active - WASD to move, mouse to look, ESC to exit'); +} + +function exitFPVMode() { + console.log('๐Ÿ”„ Exiting FPV mode'); + + // Exit pointer lock first, before changing fpvMode + if (document.pointerLockElement) { + document.exitPointerLock(); + } + + // Small delay to ensure pointer lock has been released + setTimeout(() => { + fpvMode = false; + + // Re-enable orbit controls + if (window.controls) { + window.controls.enabled = true; + } + + // Hide crosshair + const crosshair = document.getElementById('fpv-crosshair'); + if (crosshair) { + crosshair.style.display = 'none'; + } + + // Update button text + const fpvBtn = document.getElementById('fpv-btn'); + if (fpvBtn) { + fpvBtn.textContent = 'Enter FPV Mode'; + fpvBtn.style.background = '#2196F3'; + } + + // Reset movement controls + fpvControls.moveForward = false; + fpvControls.moveBackward = false; + fpvControls.moveLeft = false; + fpvControls.moveRight = false; + fpvControls.jump = false; + fpvControls.crouch = false; + fpvControls.verticalVelocity = 0; + fpvControls.isJumping = false; + fpvControls.isCrouching = false; + fpvControls.currentHeight = window.eyeHeight; + fpvControls.targetHeight = window.eyeHeight; + + console.log('Orbit controls restored'); + }, 50); +} + +export function updateFPVMovement(camera) { + if (!fpvMode) return; + + const delta = 0.016; // Approximate 60fps + const moveSpeed = 100.0; // Units per second (1000x faster) + const jumpSpeed = 220.0; // Jump initial velocity (4x higher) + const gravity = 420.0; // Gravity strength (4x faster) + const crouchHeight = window.eyeHeight * 0.5; // Crouch height (60% of normal eye height) + const crouchSpeed = 100.0; // Speed of crouching transition + + // Reset direction + fpvControls.direction.set(0, 0, 0); + + // Calculate movement direction based on current rotation + const forward = new THREE.Vector3(0, 0, -1); + const right = new THREE.Vector3(1, 0, 0); + + // Apply horizontal rotation to movement vectors + forward.applyAxisAngle(new THREE.Vector3(0, 1, 0), fpvControls.rotation.y); + right.applyAxisAngle(new THREE.Vector3(0, 1, 0), fpvControls.rotation.y); + + // Apply movement inputs + if (fpvControls.moveForward) fpvControls.direction.add(forward); + if (fpvControls.moveBackward) fpvControls.direction.sub(forward); + if (fpvControls.moveLeft) fpvControls.direction.sub(right); + if (fpvControls.moveRight) fpvControls.direction.add(right); + + // Normalize and apply speed + if (fpvControls.direction.length() > 0) { + fpvControls.direction.normalize(); + fpvControls.direction.multiplyScalar(moveSpeed * delta); + + // Apply movement to camera position + camera.position.add(fpvControls.direction); + } + + // Handle jumping + if (fpvControls.jump && fpvControls.canJump && !fpvControls.isJumping) { + fpvControls.verticalVelocity = jumpSpeed; + fpvControls.isJumping = true; + fpvControls.canJump = false; + } + + // Update target height based on crouching state + fpvControls.targetHeight = fpvControls.isCrouching ? crouchHeight : window.eyeHeight; + + // Apply gravity and vertical movement + if (fpvControls.isJumping) { + fpvControls.verticalVelocity -= gravity * delta; + camera.position.y += fpvControls.verticalVelocity * delta; + + // Check if landed back on ground + if (camera.position.y <= fpvControls.targetHeight) { + camera.position.y = fpvControls.targetHeight; + fpvControls.currentHeight = fpvControls.targetHeight; + fpvControls.verticalVelocity = 0; + fpvControls.isJumping = false; + fpvControls.canJump = true; + } + } else { + // Smooth crouching transition when not jumping + if (Math.abs(fpvControls.currentHeight - fpvControls.targetHeight) > 0.01) { + const direction = fpvControls.targetHeight > fpvControls.currentHeight ? 1 : -1; + fpvControls.currentHeight += direction * crouchSpeed * delta; + + // Clamp to target + if (direction > 0 && fpvControls.currentHeight >= fpvControls.targetHeight) { + fpvControls.currentHeight = fpvControls.targetHeight; + } else if (direction < 0 && fpvControls.currentHeight <= fpvControls.targetHeight) { + fpvControls.currentHeight = fpvControls.targetHeight; + } + } + camera.position.y = fpvControls.currentHeight; + } + + // Apply rotation to camera + camera.rotation.set(fpvControls.rotation.x, fpvControls.rotation.y, 0, 'YXZ'); +} + +export function isFPVMode() { + return fpvMode; +} diff --git a/src/index.js b/src/index.js index dabd952..1fa826a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,151 +1,51 @@ import * as THREE from 'three'; -import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; -import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import './styles.css'; +import { loadCameraConfig } from './camera-config.js'; +import { setupLighting, updateCameraLights } from './lighting.js'; +import { loadCubeModel } from './model-loader.js'; +import { setupFPVButton, setupFPVControls, updateFPVMovement, isFPVMode } from './fpv-controls.js'; +import { setupPrintHandlers, setupPrintButton } from './print-handlers.js'; +import { createScene, onWindowResize } from './scene-setup.js'; // Global variables let scene, camera, renderer, controls; -let cube; let cameraConfig = null; -// FPV Mode variables -let fpvMode = false; -let fpvControls = { - moveForward: false, - moveBackward: false, - moveLeft: false, - moveRight: false, - jump: false, - crouch: false, - canJump: true, - velocity: new THREE.Vector3(), - direction: new THREE.Vector3(), - rotation: { x: 0, y: 0 }, - verticalVelocity: 0, - isJumping: false, - isCrouching: false, - currentHeight: 0, - targetHeight: 0 -}; -let modelBounds = null; -let groundLevel = 0; -let ceilingLevel = 0; -let eyeHeight = 0; - -// Load camera configuration from config file -async function loadCameraConfig() { - try { - const response = await fetch('./camera-config.json'); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - cameraConfig = await response.json(); - console.log('โœ… Camera config loaded successfully:', cameraConfig); - return cameraConfig; - } catch (error) { - console.warn('โš ๏ธ Could not load camera-config.json, using original defaults.'); - console.log('๐Ÿ“ To use custom camera settings, ensure camera-config.json is accessible from your web server.'); - - // Return original simple defaults - return { - camera: { - position: { x: 20, y: 20, z: 20 }, - target: { x: 0, y: 0, z: 0 }, - distance: 34.64 - } - }; - } -} - // Initialize the 3D scene async function init() { // Load camera configuration first cameraConfig = await loadCameraConfig(); - // Create container - const container = document.getElementById('container'); - // Create scene - scene = new THREE.Scene(); - scene.background = new THREE.Color(0x2c3e50); + // Create scene, camera, renderer, and controls + const sceneComponents = createScene(cameraConfig); + scene = sceneComponents.scene; + camera = sceneComponents.camera; + renderer = sceneComponents.renderer; + controls = sceneComponents.controls; - // Create camera with extended far clipping plane for better zoom out - camera = new THREE.PerspectiveCamera( - 75, // Field of view - window.innerWidth / window.innerHeight, // Aspect ratio - 0.1, // Near clipping plane - 10000 // Extended far clipping plane for better zoom out - ); - // Use camera config values or defaults - const camPos = cameraConfig.camera.position; - const camTarget = cameraConfig.camera.target; - camera.position.set(camPos.x, camPos.y, camPos.z); - camera.lookAt(camTarget.x, camTarget.y, camTarget.z); - - // Create renderer - renderer = new THREE.WebGLRenderer({ - antialias: true, - alpha: true - }); - renderer.setSize(window.innerWidth, window.innerHeight); - renderer.shadowMap.enabled = true; - renderer.shadowMap.type = THREE.PCFSoftShadowMap; - renderer.outputColorSpace = THREE.SRGBColorSpace; - renderer.toneMapping = THREE.ACESFilmicToneMapping; - renderer.toneMappingExposure = 1.5; - - // Enable proper transparency rendering - renderer.sortObjects = true; - renderer.autoClear = false; - - container.appendChild(renderer.domElement); - - // Add orbit controls with enhanced touch support - controls = new OrbitControls(camera, renderer.domElement); - controls.enableDamping = true; - controls.dampingFactor = 0.05; - controls.enableZoom = true; - controls.enablePan = true; - controls.enableRotate = true; - - // Touch-specific settings - controls.touches = { - ONE: THREE.TOUCH.ROTATE, // Single finger touch for rotation - TWO: THREE.TOUCH.DOLLY_PAN // Two fingers for zoom and pan - }; - - // Enhanced touch sensitivity and limits - controls.rotateSpeed = 1.0; // Rotation speed - controls.zoomSpeed = 2.0; // Increased zoom speed for faster mouse wheel - controls.panSpeed = 0.8; // Pan speed - // Sync zoom limits with camera clipping planes - controls.minDistance = 0.1; // Match camera near plane - controls.maxDistance = 8000; // Stay within camera far plane (10000) with buffer - - // Smooth touch interactions - controls.enableDamping = true; - controls.dampingFactor = 0.08; // Slightly higher for smoother touch - - // Add custom touch event handlers - setupCustomTouchControls(); + // Make scene components globally accessible + window.scene = scene; + window.camera = camera; + window.renderer = renderer; + window.controls = controls; // Add lighting - setupLighting(); + setupLighting(scene, camera); // Load the cube model - loadCubeModel(); + loadCubeModel(scene, camera, controls); // Handle window resize - window.addEventListener('resize', onWindowResize, false); + window.addEventListener('resize', () => onWindowResize(camera, renderer), false); // Handle print events - setupPrintHandlers(); + setupPrintHandlers(scene, renderer); // Setup manual print button - setupPrintButton(); + setupPrintButton(scene, renderer); // Setup FPV button - setupFPVButton(); + setupFPVButton(renderer); // Setup FPV controls setupFPVControls(); @@ -154,1002 +54,26 @@ async function init() { animate(); } -function setupCustomTouchControls() { - const canvas = renderer.domElement; - - // Add touch-friendly CSS for better interaction - canvas.style.touchAction = 'none'; - canvas.style.userSelect = 'none'; - canvas.style.webkitUserSelect = 'none'; - canvas.style.mozUserSelect = 'none'; - canvas.style.msUserSelect = 'none'; - - // Prevent context menu on long press - canvas.addEventListener('contextmenu', function(event) { - event.preventDefault(); - }, { passive: false }); - - console.log('Touch controls enabled - OrbitControls handles all touch gestures natively'); -} - -function setupPrintHandlers() { - const originalBackgroundColor = scene.background; - const originalRendererClearColor = renderer.getClearColor(new THREE.Color()); - const originalRendererClearAlpha = renderer.getClearAlpha(); - - let isTestMode = false; - let testTimeout = null; - - // Before print - change background to white and make renderer opaque - const beforePrint = () => { - if (isTestMode) { - console.log('๐Ÿšซ Print event ignored - test mode is active'); - return; - } - - console.log('๐Ÿ–จ๏ธ PRINT MODE ACTIVATED - Changing background to white'); - - // Force scene background to white - scene.background = new THREE.Color(0xffffff); - - // Set renderer clear color to white - renderer.setClearColor(0xffffff, 1.0); - - // Force canvas background to white with multiple methods - const canvas = renderer.domElement; - canvas.style.setProperty('background', 'white', 'important'); - canvas.style.setProperty('background-color', 'white', 'important'); - canvas.style.setProperty('background-image', 'none', 'important'); - - // Also set body and html backgrounds - document.body.style.setProperty('background', 'white', 'important'); - document.body.style.setProperty('background-color', 'white', 'important'); - document.documentElement.style.setProperty('background', 'white', 'important'); - document.documentElement.style.setProperty('background-color', 'white', 'important'); - - // Force a render to apply changes immediately - renderer.render(scene, camera); - - console.log('โœ… Print background forcibly set to white'); - }; - - // After print - restore original background - const afterPrint = () => { - if (isTestMode) { - console.log('๐Ÿšซ Print restore ignored - test mode is active'); - return; - } - - console.log('๐Ÿ–จ๏ธ PRINT MODE DEACTIVATED - Restoring original background'); - - // Restore original settings - scene.background = originalBackgroundColor; - renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha); - - // Remove forced styles - const canvas = renderer.domElement; - canvas.style.removeProperty('background'); - canvas.style.removeProperty('background-color'); - canvas.style.removeProperty('background-image'); - - document.body.style.removeProperty('background'); - document.body.style.removeProperty('background-color'); - document.documentElement.style.removeProperty('background'); - document.documentElement.style.removeProperty('background-color'); - - console.log('โœ… Original background restored'); - }; - - // Apply white background (used by both test and print modes) - const applyWhiteBackground = () => { - // Force scene background to white - scene.background = new THREE.Color(0xffffff); - - // Set renderer clear color to white - renderer.setClearColor(0xffffff, 1.0); - - // Force canvas background to white with multiple methods - const canvas = renderer.domElement; - canvas.style.setProperty('background', 'white', 'important'); - canvas.style.setProperty('background-color', 'white', 'important'); - canvas.style.setProperty('background-image', 'none', 'important'); - - // Also set body and html backgrounds - document.body.style.setProperty('background', 'white', 'important'); - document.body.style.setProperty('background-color', 'white', 'important'); - document.documentElement.style.setProperty('background', 'white', 'important'); - document.documentElement.style.setProperty('background-color', 'white', 'important'); - - // Force a render to apply changes immediately - renderer.render(scene, camera); - }; - - // Restore original background (used by both test and print modes) - const restoreBackground = () => { - // Restore original settings - scene.background = originalBackgroundColor; - renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha); - - // Remove forced styles - const canvas = renderer.domElement; - canvas.style.removeProperty('background'); - canvas.style.removeProperty('background-color'); - canvas.style.removeProperty('background-image'); - - document.body.style.removeProperty('background'); - document.body.style.removeProperty('background-color'); - document.documentElement.style.removeProperty('background'); - document.documentElement.style.removeProperty('background-color'); - }; - - // Test the print handler immediately (for debugging) - window.testPrintMode = () => { - console.log('๐Ÿงช Testing print mode (print events disabled during test)...'); - isTestMode = true; - - if (testTimeout) { - clearTimeout(testTimeout); - } - - applyWhiteBackground(); - console.log('โœ… Test mode background set to white'); - - testTimeout = setTimeout(() => { - console.log('๐Ÿงช Test complete, restoring...'); - restoreBackground(); - isTestMode = false; - testTimeout = null; - console.log('โœ… Test mode complete, print events re-enabled'); - }, 6000); - }; - - // Cancel test mode early - window.cancelTestMode = () => { - if (testTimeout) { - clearTimeout(testTimeout); - testTimeout = null; - } - if (isTestMode) { - restoreBackground(); - isTestMode = false; - console.log('๐Ÿงช Test mode cancelled, print events re-enabled'); - } - }; - - // Listen for print events - if (window.matchMedia) { - const mediaQueryList = window.matchMedia('print'); - mediaQueryList.addEventListener('change', (mql) => { - console.log('๐Ÿ“ฑ Media query change detected:', mql.matches ? 'PRINT' : 'SCREEN'); - if (mql.matches) { - beforePrint(); - } else { - afterPrint(); - } - }); - } - - // Fallback for older browsers - window.addEventListener('beforeprint', () => { - console.log('๐Ÿ–จ๏ธ beforeprint event fired'); - beforePrint(); - }); - window.addEventListener('afterprint', () => { - console.log('๐Ÿ–จ๏ธ afterprint event fired'); - afterPrint(); - }); - - console.log('๐Ÿ”ง Print event handlers set up.'); - console.log(' Use window.testPrintMode() to test for 6 seconds'); - console.log(' Use window.cancelTestMode() to end test early'); -} - -function setupPrintButton() { - const printBtn = document.getElementById('print-btn'); - if (!printBtn) { - console.warn('Print button not found'); - return; - } - - const originalBackgroundColor = scene.background; - const originalRendererClearColor = renderer.getClearColor(new THREE.Color()); - const originalRendererClearAlpha = renderer.getClearAlpha(); - - printBtn.addEventListener('click', () => { - console.log('๐Ÿ–จ๏ธ Manual print button clicked - forcing white background'); - - // Force white background immediately - scene.background = new THREE.Color(0xffffff); - renderer.setClearColor(0xffffff, 1.0); - - // Force canvas and page backgrounds to white - const canvas = renderer.domElement; - canvas.style.setProperty('background', 'white', 'important'); - canvas.style.setProperty('background-color', 'white', 'important'); - canvas.style.setProperty('background-image', 'none', 'important'); - - document.body.style.setProperty('background', 'white', 'important'); - document.body.style.setProperty('background-color', 'white', 'important'); - document.documentElement.style.setProperty('background', 'white', 'important'); - document.documentElement.style.setProperty('background-color', 'white', 'important'); - - // Force a render to apply changes - renderer.render(scene, camera); - - console.log('โœ… Background forced to white, opening print dialog...'); - - // Small delay to ensure changes are applied, then open print dialog - setTimeout(() => { - window.print(); - - // Restore background after a delay to account for print dialog - setTimeout(() => { - console.log('๐Ÿ”„ Restoring original background...'); - - scene.background = originalBackgroundColor; - renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha); - - canvas.style.removeProperty('background'); - canvas.style.removeProperty('background-color'); - canvas.style.removeProperty('background-image'); - - document.body.style.removeProperty('background'); - document.body.style.removeProperty('background-color'); - document.documentElement.style.removeProperty('background'); - document.documentElement.style.removeProperty('background-color'); - - console.log('โœ… Original background restored'); - }, 1000); // Wait 1 second after print dialog opens - }, 100); // Small delay to apply changes - }); - - console.log('๐Ÿ–จ๏ธ Manual print button set up'); -} - -function setupFPVButton() { - const fpvBtn = document.getElementById('fpv-btn'); - if (!fpvBtn) { - console.warn('FPV button not found'); - return; - } - - fpvBtn.addEventListener('click', () => { - if (!fpvMode) { - enterFPVMode(); - } else { - exitFPVMode(); - } - }); - - console.log('๐Ÿšถ FPV button set up'); -} - -function setupLighting() { - // Ambient light for overall illumination - increased intensity - const ambientLight = new THREE.AmbientLight(0x404040, 1.2); - scene.add(ambientLight); - - // Directional light (like sunlight) - increased intensity - const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); - directionalLight.position.set(10, 10, 5); - directionalLight.castShadow = true; - directionalLight.shadow.mapSize.width = 2048; - directionalLight.shadow.mapSize.height = 2048; - scene.add(directionalLight); - - // Point light for additional illumination - increased intensity - const pointLight = new THREE.PointLight(0xffffff, 0.8, 100); - pointLight.position.set(-10, 10, 10); - scene.add(pointLight); - - // Camera light - follows the camera to illuminate from viewing angle - window.cameraLight = new THREE.PointLight(0xffffff, 1.0, 200); - window.cameraLight.position.copy(camera.position); - scene.add(window.cameraLight); - - // Additional camera spotlight for focused illumination - window.cameraSpotlight = new THREE.SpotLight(0xffffff, 0.8, 100, Math.PI / 4, 0.3); - window.cameraSpotlight.position.copy(camera.position); - window.cameraSpotlight.target.position.set(0, 0, 0); // Point at origin initially - scene.add(window.cameraSpotlight); - scene.add(window.cameraSpotlight.target); - - // Interior point lights (will be repositioned when model loads) - increased intensity - window.interiorLight = new THREE.PointLight(0xffffff, 0.8, 50); - window.interiorLight.position.set(0, 0, 0); // Will be moved to model center - scene.add(window.interiorLight); - - // Additional interior light for better coverage - increased intensity - window.interiorLight2 = new THREE.PointLight(0xffffff, 0.5, 30); - window.interiorLight2.position.set(5, 5, -5); // Will be adjusted relative to model - scene.add(window.interiorLight2); - - // Hemisphere light for natural lighting - increased intensity - const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8); - scene.add(hemiLight); - - console.log('โœจ Camera lights added - they will follow your view'); - - // Create environment map for glass reflections - setupEnvironmentMap(); -} - -function setupEnvironmentMap() { - // Create a simple cube texture for reflections (makes glass more visible) - const cubeTextureLoader = new THREE.CubeTextureLoader(); - - // Create a procedural cube map for reflections - const envMapRenderTarget = new THREE.WebGLCubeRenderTarget(256, { - generateMipmaps: true, - minFilter: THREE.LinearMipmapLinearFilter - }); - - // Simple gradient environment for reflections - const canvas = document.createElement('canvas'); - canvas.width = 256; - canvas.height = 256; - const ctx = canvas.getContext('2d'); - - // Create gradient for reflections - const gradient = ctx.createLinearGradient(0, 0, 0, 256); - gradient.addColorStop(0, '#87CEEB'); // Sky blue - gradient.addColorStop(0.5, '#F0F8FF'); // Alice blue - gradient.addColorStop(1, '#4682B4'); // Steel blue - - ctx.fillStyle = gradient; - ctx.fillRect(0, 0, 256, 256); - - // Convert to texture - const texture = new THREE.CanvasTexture(canvas); - texture.mapping = THREE.EquirectangularReflectionMapping; - - // Store for glass materials - window.environmentMap = texture; -} - -function loadCubeModel() { - // Create loaders - const mtlLoader = new MTLLoader(); - const objLoader = new OBJLoader(); - - // Load material file first - mtlLoader.load('cube.mtl', function(materials) { - materials.preload(); - - // Configure materials for double-sided rendering with proper transparency - Object.keys(materials.materials).forEach(function(key) { - const material = materials.materials[key]; - material.side = THREE.DoubleSide; - - // Handle glass materials specially - if (key.toLowerCase().includes('glass')) { - material.transparent = true; - - // Preserve MTL file opacity values for glass materials - // Only apply default opacity if not already set in MTL - if (material.opacity === undefined || material.opacity === 1.0) { - material.opacity = 0.7; // Fallback for visibility - } - - // Enhanced glass properties for better visibility - material.shininess = 100; - material.reflectivity = 0.8; - - // Glass-like color with slight tint for visibility - material.color = new THREE.Color(0.9, 0.95, 1.0); - - // Add specular highlights to make glass more apparent - material.specular = new THREE.Color(0.8, 0.9, 1.0); - - // Slight emissive to make glass edges more visible - if (material.emissive) { - material.emissive.setHex(0x001122); // Very subtle blue glow - } - - // Apply environment map for reflections - if (window.environmentMap) { - material.envMap = window.environmentMap; - material.envMapIntensity = 0.9; - } - - console.log(`Glass material '${key}' configured with opacity: ${material.opacity}`); - } else { - // Non-glass materials - material.transparent = false; - material.opacity = 1.0; - - // Add emissive for interior visibility on solid materials - if (material.emissive) { - material.emissive.setHex(0x222222); // Increased emissive brightness - } else { - material.emissive = new THREE.Color(0x222222); - } - } - - material.needsUpdate = true; - }); - - // Set materials to OBJ loader - objLoader.setMaterials(materials); - - // Load the OBJ file - objLoader.load('cube.obj', function(object) { - // Scale and position the model - object.scale.set(0.5, 0.5, 0.5); - object.position.set(0, 0, 0); - - // Enable shadows and fix materials for interior faces - object.traverse(function(child) { - if (child instanceof THREE.Mesh) { - child.castShadow = true; - child.receiveShadow = true; - - // Special handling for glass materials to improve visibility - if (child.material && child.material.name && child.material.name.toLowerCase().includes('glass')) { - // Glass-specific settings - child.material.side = THREE.DoubleSide; - child.material.transparent = true; - - // Preserve original opacity from MTL if available - if (child.material.opacity === undefined || child.material.opacity === 1.0) { - child.material.opacity = 0.7; - } - - child.material.alphaTest = 0.1; - - // Enhanced reflective properties - child.material.envMapIntensity = 0.9; - - // Apply environment map for reflections - if (window.environmentMap) { - child.material.envMap = window.environmentMap; - } - - // Disable shadow casting for glass (more realistic) - child.castShadow = false; - child.receiveShadow = true; - - console.log('Applied glass-specific settings to mesh'); - - // Add subtle wireframe to glass edges for better visibility - if (child.geometry) { - const wireframe = new THREE.EdgesGeometry(child.geometry); - const wireframeMaterial = new THREE.LineBasicMaterial({ - color: 0x88aaff, - transparent: true, - opacity: 0.3, - linewidth: 2 - }); - const wireframeLines = new THREE.LineSegments(wireframe, wireframeMaterial); - child.add(wireframeLines); - } - } else { - // Non-glass materials - child.material.side = THREE.DoubleSide; - child.material.transparent = false; - child.material.opacity = 1.0; - - // Ensure proper lighting on both sides - if (child.material.type === 'MeshLambertMaterial' || child.material.type === 'MeshPhongMaterial') { - child.material.emissive = new THREE.Color(0x222222); // Increased self-illumination for brightness - } - } - - child.material.needsUpdate = true; - } - }); - - // Store reference and add to scene - cube = object; - scene.add(cube); - - // Calculate model center and adjust controls - centerModelAndControls(cube); - - // Hide loading indicator - document.getElementById('loading').style.display = 'none'; - - console.log('Cube loaded successfully!'); - }, - function(progress) { - console.log('Loading progress:', (progress.loaded / progress.total * 100) + '%'); - }, - function(error) { - console.error('Error loading OBJ file:', error); - document.getElementById('loading').innerHTML = - '
Error loading model. Please check console for details.
'; - }); - }, - function(progress) { - console.log('Loading MTL progress:', (progress.loaded / progress.total * 100) + '%'); - }, - function(error) { - console.error('Error loading MTL file:', error); - // Try to load OBJ without materials - loadObjWithoutMaterials(); - }); -} - -function loadObjWithoutMaterials() { - const objLoader = new OBJLoader(); - - objLoader.load('cube.obj', function(object) { - // Apply default material with better interior visibility - const defaultMaterial = new THREE.MeshPhongMaterial({ - color: 0xffffff, - side: THREE.DoubleSide, - transparent: false, - opacity: 1.0, - emissive: new THREE.Color(0x222222), // Increased self-illumination for brightness - shininess: 30 - }); - - object.traverse(function(child) { - if (child instanceof THREE.Mesh) { - child.material = defaultMaterial.clone(); // Clone to avoid sharing - child.castShadow = true; - child.receiveShadow = true; - } - }); - - // Scale and position - object.scale.set(0.5, 0.5, 0.5); - object.position.set(0, 0, 0); - - cube = object; - scene.add(cube); - - // Calculate model center and adjust controls - centerModelAndControls(cube); - - document.getElementById('loading').style.display = 'none'; - console.log('Cube loaded without materials!'); - }, - function(progress) { - console.log('Loading progress:', (progress.loaded / progress.total * 100) + '%'); - }, - function(error) { - console.error('Error loading OBJ file:', error); - document.getElementById('loading').innerHTML = - '
Failed to load model. Check if cube.obj exists.
'; - }); -} - -function centerModelAndControls(object) { - // Calculate the bounding box of the model - const boundingBox = new THREE.Box3().setFromObject(object); - - // Get the center point of the bounding box - const center = boundingBox.getCenter(new THREE.Vector3()); - - // Get the size of the bounding box - const size = boundingBox.getSize(new THREE.Vector3()); - - // Store model bounds for FPV mode - modelBounds = { boundingBox, center, size }; - groundLevel = boundingBox.min.y; - ceilingLevel = boundingBox.max.y; - eyeHeight = groundLevel + (ceilingLevel - groundLevel) * 0.5; - - console.log('FPV bounds calculated:'); - console.log('Ground level:', groundLevel); - console.log('Ceiling level:', ceilingLevel); - console.log('Eye height (50%):', eyeHeight); - - // Set the orbit controls target to the model's center - controls.target.copy(center); - - // Calculate optimal camera distance based on model size - const maxDimension = Math.max(size.x, size.y, size.z); - const fov = camera.fov * (Math.PI / 180); - const distance = Math.abs(maxDimension / Math.sin(fov / 2)) * 1.2; // 1.2 for some padding - - // Position camera at optimal distance from model center - const direction = camera.position.clone().sub(center).normalize(); - camera.position.copy(center).add(direction.multiplyScalar(distance)); - - // Update camera to look at the model center - camera.lookAt(center); - - // Reposition interior lights relative to model center - if (window.interiorLight) { - window.interiorLight.position.copy(center); - - // Adjust light range based on model size - window.interiorLight.distance = maxDimension * 2; - } - - if (window.interiorLight2) { - window.interiorLight2.position.copy(center).add(new THREE.Vector3( - size.x * 0.3, - size.y * 0.3, - -size.z * 0.3 - )); - window.interiorLight2.distance = maxDimension * 1.5; - } - - // Update camera lights to target the model center - if (window.cameraLight) { - window.cameraLight.distance = maxDimension * 3; // Adjust range based on model size - } - - if (window.cameraSpotlight) { - window.cameraSpotlight.target.position.copy(center); - window.cameraSpotlight.distance = maxDimension * 2; - } - - // Update controls - controls.update(); - - // Add rotation logging - setupRotationLogging(); - - console.log('Model centered at:', center); - console.log('Model size:', size); - console.log('Camera positioned at:', camera.position); -} - -function setupRotationLogging() { - let debounceTimer = null; - - // Log rotation and zoom changes with 1-second debounce - controls.addEventListener('change', function() { - // Clear existing timer - if (debounceTimer) { - clearTimeout(debounceTimer); - } - - // Set new timer for 1 second delay - debounceTimer = setTimeout(() => { - const distance = camera.position.distanceTo(controls.target); - const position = camera.position.clone(); - const target = controls.target.clone(); - - // Calculate spherical coordinates for rotation - const spherical = new THREE.Spherical().setFromVector3( - position.clone().sub(target) - ); - - console.log('=== CAMERA STATE ==='); - console.log('Position:', { - x: Math.round(position.x * 100) / 100, - y: Math.round(position.y * 100) / 100, - z: Math.round(position.z * 100) / 100 - }); - console.log('Target:', { - x: Math.round(target.x * 100) / 100, - y: Math.round(target.y * 100) / 100, - z: Math.round(target.z * 100) / 100 - }); - console.log('Distance (Zoom):', Math.round(distance * 100) / 100); - console.log('Spherical Rotation:'); - console.log(' - Radius:', Math.round(spherical.radius * 100) / 100); - console.log(' - Theta (horizontal):', Math.round(spherical.theta * 100) / 100, 'radians'); - console.log(' - Phi (vertical):', Math.round(spherical.phi * 100) / 100, 'radians'); - console.log('=================='); - - debounceTimer = null; - }, 1000); - }); - - console.log('Rotation logging enabled - camera state will be logged 1 second after movement stops'); -} - -function setupFPVControls() { - // Keyboard event handlers - document.addEventListener('keydown', onKeyDown); - document.addEventListener('keyup', onKeyUp); - - // Mouse event handlers for FPV mode - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('click', onMouseClick); - - console.log('FPV controls setup complete. Press F to enter FPV mode'); -} - -function onKeyDown(event) { - switch (event.code) { - case 'KeyF': - event.preventDefault(); - event.stopPropagation(); - if (!fpvMode) { - enterFPVMode(); - } else { - exitFPVMode(); - } - return false; - case 'Escape': - if (fpvMode) { - event.preventDefault(); - event.stopPropagation(); - exitFPVMode(); - return false; - } - break; - case 'KeyW': - if (fpvMode) fpvControls.moveForward = true; - break; - case 'KeyA': - if (fpvMode) fpvControls.moveLeft = true; - break; - case 'KeyS': - if (fpvMode) fpvControls.moveBackward = true; - break; - case 'KeyD': - if (fpvMode) fpvControls.moveRight = true; - break; - case 'Space': - if (fpvMode) { - event.preventDefault(); - fpvControls.jump = true; - } - break; - case 'ShiftLeft': - case 'ShiftRight': - if (fpvMode) { - event.preventDefault(); - fpvControls.crouch = true; - fpvControls.isCrouching = true; - } - break; - } -} - -function onKeyUp(event) { - switch (event.code) { - case 'KeyW': - if (fpvMode) fpvControls.moveForward = false; - break; - case 'KeyA': - if (fpvMode) fpvControls.moveLeft = false; - break; - case 'KeyS': - if (fpvMode) fpvControls.moveBackward = false; - break; - case 'KeyD': - if (fpvMode) fpvControls.moveRight = false; - break; - case 'Space': - if (fpvMode) fpvControls.jump = false; - break; - case 'ShiftLeft': - case 'ShiftRight': - if (fpvMode) { - fpvControls.crouch = false; - fpvControls.isCrouching = false; - } - break; - } -} - -function onMouseMove(event) { - if (!fpvMode) return; - - const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; - const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; - - fpvControls.rotation.y -= movementX * 0.002; // Horizontal rotation (yaw) - fpvControls.rotation.x -= movementY * 0.002; // Vertical rotation (pitch) - - // Limit vertical look angle - fpvControls.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, fpvControls.rotation.x)); -} - -function onMouseClick(event) { - if (fpvMode) { - // Request pointer lock for mouse control - renderer.domElement.requestPointerLock(); - } -} - -function enterFPVMode() { - if (!modelBounds) { - console.warn('Model not loaded yet, cannot enter FPV mode'); - return; - } - - console.log('๐Ÿšถ Entering FPV mode'); - fpvMode = true; - - // Disable orbit controls - controls.enabled = false; - - // Keep current camera position instead of forcing to model center - // Just ensure the height is reasonable for walking - fpvControls.currentHeight = Math.max(camera.position.y, eyeHeight); - fpvControls.targetHeight = fpvControls.currentHeight; - camera.position.y = fpvControls.currentHeight; - - // Calculate rotation based on current camera orientation - const euler = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ'); - fpvControls.rotation.x = euler.x; - fpvControls.rotation.y = euler.y; - - // Reset movement state - fpvControls.velocity.set(0, 0, 0); - fpvControls.verticalVelocity = 0; - fpvControls.isJumping = false; - fpvControls.isCrouching = false; - - // Show crosshair - const crosshair = document.getElementById('fpv-crosshair'); - if (crosshair) { - crosshair.style.display = 'block'; - } - - // Update button text - const fpvBtn = document.getElementById('fpv-btn'); - if (fpvBtn) { - fpvBtn.textContent = 'Exit FPV Mode'; - fpvBtn.style.background = '#FF5722'; - } - - // Request pointer lock - renderer.domElement.requestPointerLock(); - - console.log('FPV mode active - WASD to move, mouse to look, ESC to exit'); -} - -function exitFPVMode() { - console.log('๐Ÿ”„ Exiting FPV mode'); - - // Exit pointer lock first, before changing fpvMode - if (document.pointerLockElement) { - document.exitPointerLock(); - } - - // Small delay to ensure pointer lock has been released - setTimeout(() => { - fpvMode = false; - - // Re-enable orbit controls - controls.enabled = true; - - // Hide crosshair - const crosshair = document.getElementById('fpv-crosshair'); - if (crosshair) { - crosshair.style.display = 'none'; - } - - // Update button text - const fpvBtn = document.getElementById('fpv-btn'); - if (fpvBtn) { - fpvBtn.textContent = 'Enter FPV Mode'; - fpvBtn.style.background = '#2196F3'; - } - - // Reset movement controls - fpvControls.moveForward = false; - fpvControls.moveBackward = false; - fpvControls.moveLeft = false; - fpvControls.moveRight = false; - fpvControls.jump = false; - fpvControls.crouch = false; - fpvControls.verticalVelocity = 0; - fpvControls.isJumping = false; - fpvControls.isCrouching = false; - fpvControls.currentHeight = eyeHeight; - fpvControls.targetHeight = eyeHeight; - - console.log('Orbit controls restored'); - }, 50); -} - -function updateFPVMovement() { - if (!fpvMode) return; - - const delta = 0.016; // Approximate 60fps - const moveSpeed = 100.0; // Units per second (1000x faster) - const jumpSpeed = 220.0; // Jump initial velocity (4x higher) - const gravity = 420.0; // Gravity strength (4x faster) - const crouchHeight = eyeHeight * 0.5; // Crouch height (60% of normal eye height) - const crouchSpeed = 100.0; // Speed of crouching transition - - // Reset direction - fpvControls.direction.set(0, 0, 0); - - // Calculate movement direction based on current rotation - const forward = new THREE.Vector3(0, 0, -1); - const right = new THREE.Vector3(1, 0, 0); - - // Apply horizontal rotation to movement vectors - forward.applyAxisAngle(new THREE.Vector3(0, 1, 0), fpvControls.rotation.y); - right.applyAxisAngle(new THREE.Vector3(0, 1, 0), fpvControls.rotation.y); - - // Apply movement inputs - if (fpvControls.moveForward) fpvControls.direction.add(forward); - if (fpvControls.moveBackward) fpvControls.direction.sub(forward); - if (fpvControls.moveLeft) fpvControls.direction.sub(right); - if (fpvControls.moveRight) fpvControls.direction.add(right); - - // Normalize and apply speed - if (fpvControls.direction.length() > 0) { - fpvControls.direction.normalize(); - fpvControls.direction.multiplyScalar(moveSpeed * delta); - - // Apply movement to camera position - camera.position.add(fpvControls.direction); - } - - // Handle jumping - if (fpvControls.jump && fpvControls.canJump && !fpvControls.isJumping) { - fpvControls.verticalVelocity = jumpSpeed; - fpvControls.isJumping = true; - fpvControls.canJump = false; - } - - // Update target height based on crouching state - fpvControls.targetHeight = fpvControls.isCrouching ? crouchHeight : eyeHeight; - - // Apply gravity and vertical movement - if (fpvControls.isJumping) { - fpvControls.verticalVelocity -= gravity * delta; - camera.position.y += fpvControls.verticalVelocity * delta; - - // Check if landed back on ground - if (camera.position.y <= fpvControls.targetHeight) { - camera.position.y = fpvControls.targetHeight; - fpvControls.currentHeight = fpvControls.targetHeight; - fpvControls.verticalVelocity = 0; - fpvControls.isJumping = false; - fpvControls.canJump = true; - } - } else { - // Smooth crouching transition when not jumping - if (Math.abs(fpvControls.currentHeight - fpvControls.targetHeight) > 0.01) { - const direction = fpvControls.targetHeight > fpvControls.currentHeight ? 1 : -1; - fpvControls.currentHeight += direction * crouchSpeed * delta; - - // Clamp to target - if (direction > 0 && fpvControls.currentHeight >= fpvControls.targetHeight) { - fpvControls.currentHeight = fpvControls.targetHeight; - } else if (direction < 0 && fpvControls.currentHeight <= fpvControls.targetHeight) { - fpvControls.currentHeight = fpvControls.targetHeight; - } - } - camera.position.y = fpvControls.currentHeight; - } - - // Apply rotation to camera - camera.rotation.set(fpvControls.rotation.x, fpvControls.rotation.y, 0, 'YXZ'); -} - function animate() { requestAnimationFrame(animate); // Update FPV movement if in FPV mode - updateFPVMovement(); + updateFPVMovement(camera); // Update controls (only if not in FPV mode) - if (!fpvMode) { + if (!isFPVMode()) { controls.update(); } // Update camera lights to follow camera position - if (window.cameraLight) { - window.cameraLight.position.copy(camera.position); - } - - if (window.cameraSpotlight) { - window.cameraSpotlight.position.copy(camera.position); - // Make spotlight point towards the orbit controls target (model center) - if (controls && controls.target) { - window.cameraSpotlight.target.position.copy(controls.target); - } - } + updateCameraLights(camera, controls); // Clear and render with proper transparency sorting renderer.clear(); renderer.render(scene, camera); } -function onWindowResize() { - camera.aspect = window.innerWidth / window.innerHeight; - camera.updateProjectionMatrix(); - renderer.setSize(window.innerWidth, window.innerHeight); -} - // Initialize when page loads window.addEventListener('load', async () => { await init(); -}); +}); \ No newline at end of file diff --git a/src/lighting.js b/src/lighting.js new file mode 100644 index 0000000..570d949 --- /dev/null +++ b/src/lighting.js @@ -0,0 +1,131 @@ +import * as THREE from 'three'; + +export function setupLighting(scene, camera) { + // Ambient light for overall illumination - increased intensity + const ambientLight = new THREE.AmbientLight(0x404040, 1.2); + scene.add(ambientLight); + + // Directional light (like sunlight) - increased intensity + const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); + directionalLight.position.set(10, 10, 5); + directionalLight.castShadow = true; + directionalLight.shadow.mapSize.width = 2048; + directionalLight.shadow.mapSize.height = 2048; + scene.add(directionalLight); + + // Point light for additional illumination - increased intensity + const pointLight = new THREE.PointLight(0xffffff, 0.8, 100); + pointLight.position.set(-10, 10, 10); + scene.add(pointLight); + + // Camera light - follows the camera to illuminate from viewing angle + window.cameraLight = new THREE.PointLight(0xffffff, 1.0, 200); + window.cameraLight.position.copy(camera.position); + scene.add(window.cameraLight); + + // Additional camera spotlight for focused illumination + window.cameraSpotlight = new THREE.SpotLight(0xffffff, 0.8, 100, Math.PI / 4, 0.3); + window.cameraSpotlight.position.copy(camera.position); + window.cameraSpotlight.target.position.set(0, 0, 0); // Point at origin initially + scene.add(window.cameraSpotlight); + scene.add(window.cameraSpotlight.target); + + // Interior point lights (will be repositioned when model loads) - increased intensity + window.interiorLight = new THREE.PointLight(0xffffff, 0.8, 50); + window.interiorLight.position.set(0, 0, 0); // Will be moved to model center + scene.add(window.interiorLight); + + // Additional interior light for better coverage - increased intensity + window.interiorLight2 = new THREE.PointLight(0xffffff, 0.5, 30); + window.interiorLight2.position.set(5, 5, -5); // Will be adjusted relative to model + scene.add(window.interiorLight2); + + // Hemisphere light for natural lighting - increased intensity + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8); + scene.add(hemiLight); + + console.log('โœจ Camera lights added - they will follow your view'); + + // Create environment map for glass reflections + setupEnvironmentMap(); +} + +export function setupEnvironmentMap() { + // Create a simple cube texture for reflections (makes glass more visible) + const cubeTextureLoader = new THREE.CubeTextureLoader(); + + // Create a procedural cube map for reflections + const envMapRenderTarget = new THREE.WebGLCubeRenderTarget(256, { + generateMipmaps: true, + minFilter: THREE.LinearMipmapLinearFilter + }); + + // Simple gradient environment for reflections + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + const ctx = canvas.getContext('2d'); + + // Create gradient for reflections + const gradient = ctx.createLinearGradient(0, 0, 0, 256); + gradient.addColorStop(0, '#87CEEB'); // Sky blue + gradient.addColorStop(0.5, '#F0F8FF'); // Alice blue + gradient.addColorStop(1, '#4682B4'); // Steel blue + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, 256, 256); + + // Convert to texture + const texture = new THREE.CanvasTexture(canvas); + texture.mapping = THREE.EquirectangularReflectionMapping; + + // Store for glass materials + window.environmentMap = texture; +} + +export function updateCameraLights(camera, controls) { + // Update camera lights to follow camera position + if (window.cameraLight) { + window.cameraLight.position.copy(camera.position); + } + + if (window.cameraSpotlight) { + window.cameraSpotlight.position.copy(camera.position); + // Make spotlight point towards the orbit controls target (model center) + if (controls && controls.target) { + window.cameraSpotlight.target.position.copy(controls.target); + } + } +} + +export function adjustLightsForModel(modelBounds) { + const { center, size } = modelBounds; + const maxDimension = Math.max(size.x, size.y, size.z); + + // Reposition interior lights relative to model center + if (window.interiorLight) { + window.interiorLight.position.copy(center); + + // Adjust light range based on model size + window.interiorLight.distance = maxDimension * 2; + } + + if (window.interiorLight2) { + window.interiorLight2.position.copy(center).add(new THREE.Vector3( + size.x * 0.3, + size.y * 0.3, + -size.z * 0.3 + )); + window.interiorLight2.distance = maxDimension * 1.5; + } + + // Update camera lights to target the model center + if (window.cameraLight) { + window.cameraLight.distance = maxDimension * 3; // Adjust range based on model size + } + + if (window.cameraSpotlight) { + window.cameraSpotlight.target.position.copy(center); + window.cameraSpotlight.distance = maxDimension * 2; + } +} diff --git a/src/model-loader.js b/src/model-loader.js new file mode 100644 index 0000000..3623f04 --- /dev/null +++ b/src/model-loader.js @@ -0,0 +1,316 @@ +import * as THREE from 'three'; +import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; +import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'; +import { adjustLightsForModel } from './lighting.js'; + +export function loadCubeModel(scene, camera, controls, onLoadComplete) { + // Create loaders + const mtlLoader = new MTLLoader(); + const objLoader = new OBJLoader(); + + // Load material file first + mtlLoader.load('cube.mtl', function(materials) { + materials.preload(); + + // Configure materials for double-sided rendering with proper transparency + Object.keys(materials.materials).forEach(function(key) { + const material = materials.materials[key]; + material.side = THREE.DoubleSide; + + // Handle glass materials specially + if (key.toLowerCase().includes('glass')) { + material.transparent = true; + + // Preserve MTL file opacity values for glass materials + // Only apply default opacity if not already set in MTL + if (material.opacity === undefined || material.opacity === 1.0) { + material.opacity = 0.7; // Fallback for visibility + } + + // Enhanced glass properties for better visibility + material.shininess = 100; + material.reflectivity = 0.8; + + // Glass-like color with slight tint for visibility + material.color = new THREE.Color(0.9, 0.95, 1.0); + + // Add specular highlights to make glass more apparent + material.specular = new THREE.Color(0.8, 0.9, 1.0); + + // Slight emissive to make glass edges more visible + if (material.emissive) { + material.emissive.setHex(0x001122); // Very subtle blue glow + } + + // Apply environment map for reflections + if (window.environmentMap) { + material.envMap = window.environmentMap; + material.envMapIntensity = 0.9; + } + + console.log(`Glass material '${key}' configured with opacity: ${material.opacity}`); + } else { + // Non-glass materials + material.transparent = false; + material.opacity = 1.0; + + // Add emissive for interior visibility on solid materials + if (material.emissive) { + material.emissive.setHex(0x222222); // Increased emissive brightness + } else { + material.emissive = new THREE.Color(0x222222); + } + } + + material.needsUpdate = true; + }); + + // Set materials to OBJ loader + objLoader.setMaterials(materials); + + // Load the OBJ file + objLoader.load('cube.obj', function(object) { + // Scale and position the model + object.scale.set(0.5, 0.5, 0.5); + object.position.set(0, 0, 0); + + // Enable shadows and fix materials for interior faces + object.traverse(function(child) { + if (child instanceof THREE.Mesh) { + child.castShadow = true; + child.receiveShadow = true; + + // Special handling for glass materials to improve visibility + if (child.material && child.material.name && child.material.name.toLowerCase().includes('glass')) { + // Glass-specific settings + child.material.side = THREE.DoubleSide; + child.material.transparent = true; + + // Preserve original opacity from MTL if available + if (child.material.opacity === undefined || child.material.opacity === 1.0) { + child.material.opacity = 0.7; + } + + child.material.alphaTest = 0.1; + + // Enhanced reflective properties + child.material.envMapIntensity = 0.9; + + // Apply environment map for reflections + if (window.environmentMap) { + child.material.envMap = window.environmentMap; + } + + // Disable shadow casting for glass (more realistic) + child.castShadow = false; + child.receiveShadow = true; + + console.log('Applied glass-specific settings to mesh'); + + // Add subtle wireframe to glass edges for better visibility + if (child.geometry) { + const wireframe = new THREE.EdgesGeometry(child.geometry); + const wireframeMaterial = new THREE.LineBasicMaterial({ + color: 0x88aaff, + transparent: true, + opacity: 0.3, + linewidth: 2 + }); + const wireframeLines = new THREE.LineSegments(wireframe, wireframeMaterial); + child.add(wireframeLines); + } + } else { + // Non-glass materials + child.material.side = THREE.DoubleSide; + child.material.transparent = false; + child.material.opacity = 1.0; + + // Ensure proper lighting on both sides + if (child.material.type === 'MeshLambertMaterial' || child.material.type === 'MeshPhongMaterial') { + child.material.emissive = new THREE.Color(0x222222); // Increased self-illumination for brightness + } + } + + child.material.needsUpdate = true; + } + }); + + // Store reference and add to scene + window.cube = object; + scene.add(object); + + // Calculate model center and adjust controls + centerModelAndControls(object, camera, controls); + + // Hide loading indicator + document.getElementById('loading').style.display = 'none'; + + console.log('Cube loaded successfully!'); + + if (onLoadComplete) { + onLoadComplete(object); + } + }, + function(progress) { + console.log('Loading progress:', (progress.loaded / progress.total * 100) + '%'); + }, + function(error) { + console.error('Error loading OBJ file:', error); + document.getElementById('loading').innerHTML = + '
Error loading model. Please check console for details.
'; + }); + }, + function(progress) { + console.log('Loading MTL progress:', (progress.loaded / progress.total * 100) + '%'); + }, + function(error) { + console.error('Error loading MTL file:', error); + // Try to load OBJ without materials + loadObjWithoutMaterials(scene, camera, controls, onLoadComplete); + }); +} + +export function loadObjWithoutMaterials(scene, camera, controls, onLoadComplete) { + const objLoader = new OBJLoader(); + + objLoader.load('cube.obj', function(object) { + // Apply default material with better interior visibility + const defaultMaterial = new THREE.MeshPhongMaterial({ + color: 0xffffff, + side: THREE.DoubleSide, + transparent: false, + opacity: 1.0, + emissive: new THREE.Color(0x222222), // Increased self-illumination for brightness + shininess: 30 + }); + + object.traverse(function(child) { + if (child instanceof THREE.Mesh) { + child.material = defaultMaterial.clone(); // Clone to avoid sharing + child.castShadow = true; + child.receiveShadow = true; + } + }); + + // Scale and position + object.scale.set(0.5, 0.5, 0.5); + object.position.set(0, 0, 0); + + window.cube = object; + scene.add(object); + + // Calculate model center and adjust controls + centerModelAndControls(object, camera, controls); + + document.getElementById('loading').style.display = 'none'; + console.log('Cube loaded without materials!'); + + if (onLoadComplete) { + onLoadComplete(object); + } + }, + function(progress) { + console.log('Loading progress:', (progress.loaded / progress.total * 100) + '%'); + }, + function(error) { + console.error('Error loading OBJ file:', error); + document.getElementById('loading').innerHTML = + '
Failed to load model. Check if cube.obj exists.
'; + }); +} + +function centerModelAndControls(object, camera, controls) { + // Calculate the bounding box of the model + const boundingBox = new THREE.Box3().setFromObject(object); + + // Get the center point of the bounding box + const center = boundingBox.getCenter(new THREE.Vector3()); + + // Get the size of the bounding box + const size = boundingBox.getSize(new THREE.Vector3()); + + // Store model bounds for FPV mode + window.modelBounds = { boundingBox, center, size }; + window.groundLevel = boundingBox.min.y; + window.ceilingLevel = boundingBox.max.y; + window.eyeHeight = window.groundLevel + (window.ceilingLevel - window.groundLevel) * 0.5; + + console.log('FPV bounds calculated:'); + console.log('Ground level:', window.groundLevel); + console.log('Ceiling level:', window.ceilingLevel); + console.log('Eye height (50%):', window.eyeHeight); + + // Set the orbit controls target to the model's center + controls.target.copy(center); + + // Calculate optimal camera distance based on model size + const maxDimension = Math.max(size.x, size.y, size.z); + const fov = camera.fov * (Math.PI / 180); + const distance = Math.abs(maxDimension / Math.sin(fov / 2)) * 1.2; // 1.2 for some padding + + // Position camera at optimal distance from model center + const direction = camera.position.clone().sub(center).normalize(); + camera.position.copy(center).add(direction.multiplyScalar(distance)); + + // Update camera to look at the model center + camera.lookAt(center); + + // Adjust lights based on model size + adjustLightsForModel({ center, size }); + + // Update controls + controls.update(); + + // Add rotation logging + setupRotationLogging(camera, controls); + + console.log('Model centered at:', center); + console.log('Model size:', size); + console.log('Camera positioned at:', camera.position); +} + +function setupRotationLogging(camera, controls) { + let debounceTimer = null; + + // Log rotation and zoom changes with 1-second debounce + controls.addEventListener('change', function() { + // Clear existing timer + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + // Set new timer for 1 second delay + debounceTimer = setTimeout(() => { + const distance = camera.position.distanceTo(controls.target); + const position = camera.position.clone(); + const target = controls.target.clone(); + + // Calculate spherical coordinates for rotation + const spherical = new THREE.Spherical().setFromVector3( + position.clone().sub(target) + ); + + console.log('=== CAMERA STATE ==='); + console.log('Position:', { + x: Math.round(position.x * 100) / 100, + y: Math.round(position.y * 100) / 100, + z: Math.round(position.z * 100) / 100 + }); + console.log('Target:', { + x: Math.round(target.x * 100) / 100, + y: Math.round(target.y * 100) / 100, + z: Math.round(target.z * 100) / 100 + }); + console.log('Distance (Zoom):', Math.round(distance * 100) / 100); + console.log('Spherical Rotation:'); + console.log(' - Radius:', Math.round(spherical.radius * 100) / 100); + console.log(' - Theta (horizontal):', Math.round(spherical.theta * 100) / 100, 'radians'); + console.log(' - Phi (vertical):', Math.round(spherical.phi * 100) / 100, 'radians'); + console.log('=================='); + + debounceTimer = null; + }, 1000); + }); + + console.log('Rotation logging enabled - camera state will be logged 1 second after movement stops'); +} diff --git a/src/print-handlers.js b/src/print-handlers.js new file mode 100644 index 0000000..d70699c --- /dev/null +++ b/src/print-handlers.js @@ -0,0 +1,235 @@ +import * as THREE from 'three'; + +export function setupPrintHandlers(scene, renderer) { + const originalBackgroundColor = scene.background; + const originalRendererClearColor = renderer.getClearColor(new THREE.Color()); + const originalRendererClearAlpha = renderer.getClearAlpha(); + + let isTestMode = false; + let testTimeout = null; + + // Before print - change background to white and make renderer opaque + const beforePrint = () => { + if (isTestMode) { + console.log('๐Ÿšซ Print event ignored - test mode is active'); + return; + } + + console.log('๐Ÿ–จ๏ธ PRINT MODE ACTIVATED - Changing background to white'); + + // Force scene background to white + scene.background = new THREE.Color(0xffffff); + + // Set renderer clear color to white + renderer.setClearColor(0xffffff, 1.0); + + // Force canvas background to white with multiple methods + const canvas = renderer.domElement; + canvas.style.setProperty('background', 'white', 'important'); + canvas.style.setProperty('background-color', 'white', 'important'); + canvas.style.setProperty('background-image', 'none', 'important'); + + // Also set body and html backgrounds + document.body.style.setProperty('background', 'white', 'important'); + document.body.style.setProperty('background-color', 'white', 'important'); + document.documentElement.style.setProperty('background', 'white', 'important'); + document.documentElement.style.setProperty('background-color', 'white', 'important'); + + // Force a render to apply changes immediately + renderer.render(scene, window.camera); + + console.log('โœ… Print background forcibly set to white'); + }; + + // After print - restore original background + const afterPrint = () => { + if (isTestMode) { + console.log('๐Ÿšซ Print restore ignored - test mode is active'); + return; + } + + console.log('๐Ÿ–จ๏ธ PRINT MODE DEACTIVATED - Restoring original background'); + + // Restore original settings + scene.background = originalBackgroundColor; + renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha); + + // Remove forced styles + const canvas = renderer.domElement; + canvas.style.removeProperty('background'); + canvas.style.removeProperty('background-color'); + canvas.style.removeProperty('background-image'); + + document.body.style.removeProperty('background'); + document.body.style.removeProperty('background-color'); + document.documentElement.style.removeProperty('background'); + document.documentElement.style.removeProperty('background-color'); + + console.log('โœ… Original background restored'); + }; + + // Apply white background (used by both test and print modes) + const applyWhiteBackground = () => { + // Force scene background to white + scene.background = new THREE.Color(0xffffff); + + // Set renderer clear color to white + renderer.setClearColor(0xffffff, 1.0); + + // Force canvas background to white with multiple methods + const canvas = renderer.domElement; + canvas.style.setProperty('background', 'white', 'important'); + canvas.style.setProperty('background-color', 'white', 'important'); + canvas.style.setProperty('background-image', 'none', 'important'); + + // Also set body and html backgrounds + document.body.style.setProperty('background', 'white', 'important'); + document.body.style.setProperty('background-color', 'white', 'important'); + document.documentElement.style.setProperty('background', 'white', 'important'); + document.documentElement.style.setProperty('background-color', 'white', 'important'); + + // Force a render to apply changes immediately + renderer.render(scene, window.camera); + }; + + // Restore original background (used by both test and print modes) + const restoreBackground = () => { + // Restore original settings + scene.background = originalBackgroundColor; + renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha); + + // Remove forced styles + const canvas = renderer.domElement; + canvas.style.removeProperty('background'); + canvas.style.removeProperty('background-color'); + canvas.style.removeProperty('background-image'); + + document.body.style.removeProperty('background'); + document.body.style.removeProperty('background-color'); + document.documentElement.style.removeProperty('background'); + document.documentElement.style.removeProperty('background-color'); + }; + + // Test the print handler immediately (for debugging) + window.testPrintMode = () => { + console.log('๐Ÿงช Testing print mode (print events disabled during test)...'); + isTestMode = true; + + if (testTimeout) { + clearTimeout(testTimeout); + } + + applyWhiteBackground(); + console.log('โœ… Test mode background set to white'); + + testTimeout = setTimeout(() => { + console.log('๐Ÿงช Test complete, restoring...'); + restoreBackground(); + isTestMode = false; + testTimeout = null; + console.log('โœ… Test mode complete, print events re-enabled'); + }, 6000); + }; + + // Cancel test mode early + window.cancelTestMode = () => { + if (testTimeout) { + clearTimeout(testTimeout); + testTimeout = null; + } + if (isTestMode) { + restoreBackground(); + isTestMode = false; + console.log('๐Ÿงช Test mode cancelled, print events re-enabled'); + } + }; + + // Listen for print events + if (window.matchMedia) { + const mediaQueryList = window.matchMedia('print'); + mediaQueryList.addEventListener('change', (mql) => { + console.log('๐Ÿ“ฑ Media query change detected:', mql.matches ? 'PRINT' : 'SCREEN'); + if (mql.matches) { + beforePrint(); + } else { + afterPrint(); + } + }); + } + + // Fallback for older browsers + window.addEventListener('beforeprint', () => { + console.log('๐Ÿ–จ๏ธ beforeprint event fired'); + beforePrint(); + }); + window.addEventListener('afterprint', () => { + console.log('๐Ÿ–จ๏ธ afterprint event fired'); + afterPrint(); + }); + + console.log('๐Ÿ”ง Print event handlers set up.'); + console.log(' Use window.testPrintMode() to test for 6 seconds'); + console.log(' Use window.cancelTestMode() to end test early'); +} + +export function setupPrintButton(scene, renderer) { + const printBtn = document.getElementById('print-btn'); + if (!printBtn) { + console.warn('Print button not found'); + return; + } + + const originalBackgroundColor = scene.background; + const originalRendererClearColor = renderer.getClearColor(new THREE.Color()); + const originalRendererClearAlpha = renderer.getClearAlpha(); + + printBtn.addEventListener('click', () => { + console.log('๐Ÿ–จ๏ธ Manual print button clicked - forcing white background'); + + // Force white background immediately + scene.background = new THREE.Color(0xffffff); + renderer.setClearColor(0xffffff, 1.0); + + // Force canvas and page backgrounds to white + const canvas = renderer.domElement; + canvas.style.setProperty('background', 'white', 'important'); + canvas.style.setProperty('background-color', 'white', 'important'); + canvas.style.setProperty('background-image', 'none', 'important'); + + document.body.style.setProperty('background', 'white', 'important'); + document.body.style.setProperty('background-color', 'white', 'important'); + document.documentElement.style.setProperty('background', 'white', 'important'); + document.documentElement.style.setProperty('background-color', 'white', 'important'); + + // Force a render to apply changes + renderer.render(scene, window.camera); + + console.log('โœ… Background forced to white, opening print dialog...'); + + // Small delay to ensure changes are applied, then open print dialog + setTimeout(() => { + window.print(); + + // Restore background after a delay to account for print dialog + setTimeout(() => { + console.log('๐Ÿ”„ Restoring original background...'); + + scene.background = originalBackgroundColor; + renderer.setClearColor(originalRendererClearColor, originalRendererClearAlpha); + + canvas.style.removeProperty('background'); + canvas.style.removeProperty('background-color'); + canvas.style.removeProperty('background-image'); + + document.body.style.removeProperty('background'); + document.body.style.removeProperty('background-color'); + document.documentElement.style.removeProperty('background'); + document.documentElement.style.removeProperty('background-color'); + + console.log('โœ… Original background restored'); + }, 1000); // Wait 1 second after print dialog opens + }, 100); // Small delay to apply changes + }); + + console.log('๐Ÿ–จ๏ธ Manual print button set up'); +} diff --git a/src/scene-setup.js b/src/scene-setup.js new file mode 100644 index 0000000..bcf72fb --- /dev/null +++ b/src/scene-setup.js @@ -0,0 +1,97 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; + +export function createScene(cameraConfig) { + // Create container + const container = document.getElementById('container'); + + // Create scene + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x2c3e50); + + // Create camera with extended far clipping plane for better zoom out + const camera = new THREE.PerspectiveCamera( + 75, // Field of view + window.innerWidth / window.innerHeight, // Aspect ratio + 0.1, // Near clipping plane + 10000 // Extended far clipping plane for better zoom out + ); + // Use camera config values or defaults + const camPos = cameraConfig.camera.position; + const camTarget = cameraConfig.camera.target; + camera.position.set(camPos.x, camPos.y, camPos.z); + camera.lookAt(camTarget.x, camTarget.y, camTarget.z); + + // Create renderer + const renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: true + }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + renderer.outputColorSpace = THREE.SRGBColorSpace; + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1.5; + + // Enable proper transparency rendering + renderer.sortObjects = true; + renderer.autoClear = false; + + container.appendChild(renderer.domElement); + + // Add orbit controls with enhanced touch support + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.05; + controls.enableZoom = true; + controls.enablePan = true; + controls.enableRotate = true; + + // Touch-specific settings + controls.touches = { + ONE: THREE.TOUCH.ROTATE, // Single finger touch for rotation + TWO: THREE.TOUCH.DOLLY_PAN // Two fingers for zoom and pan + }; + + // Enhanced touch sensitivity and limits + controls.rotateSpeed = 1.0; // Rotation speed + controls.zoomSpeed = 2.0; // Increased zoom speed for faster mouse wheel + controls.panSpeed = 0.8; // Pan speed + // Sync zoom limits with camera clipping planes + controls.minDistance = 0.1; // Match camera near plane + controls.maxDistance = 8000; // Stay within camera far plane (10000) with buffer + + // Smooth touch interactions + controls.enableDamping = true; + controls.dampingFactor = 0.08; // Slightly higher for smoother touch + + // Add custom touch event handlers + setupCustomTouchControls(renderer); + + return { scene, camera, renderer, controls }; +} + +export function setupCustomTouchControls(renderer) { + const canvas = renderer.domElement; + + // Add touch-friendly CSS for better interaction + canvas.style.touchAction = 'none'; + canvas.style.userSelect = 'none'; + canvas.style.webkitUserSelect = 'none'; + canvas.style.mozUserSelect = 'none'; + canvas.style.msUserSelect = 'none'; + + // Prevent context menu on long press + canvas.addEventListener('contextmenu', function(event) { + event.preventDefault(); + }, { passive: false }); + + console.log('Touch controls enabled - OrbitControls handles all touch gestures natively'); +} + +export function onWindowResize(camera, renderer) { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}