CharacterVerse

Immerse yourself in an expansive 3D universe where every character has a story. Choose your avatar and embark on an unforgettable journey through dynamic worlds filled with interactive companions.

Real-time 3D Graphics
Interactive Characters
Dynamic Environments

Choose Your Champion

Select the perfect avatar that represents your journey. Each character comes with unique abilities and personality traits.

Forge Your Universe

Select 2 unique characters to populate your world. Each will bring their own stories and interactions to your adventure.

0 / 2 Characters Selected
WASD: Move
Space: Jump
Shift: Run
Near NPC: Talk (Space)
C: Camera Mode
M: Toggle Music
Camera: Follow

Character Name

Hello there! This is a sample dialog text that will be replaced with actual dialog content.

script.onload = () => { console.log('Three.js loaded successfully'); initThreeJS(); // Retry initialization }; script.onerror = () => { console.error('Failed to load Three.js'); alert('Three.js failed to load. Please check your internet connection and refresh the page.'); }; document.head.appendChild(script); return; } try { // Set up Three.js scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x87CEEB); // Sky blue // Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 5, 10); // Renderer const canvas = document.getElementById('canvas'); renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Clock for animations clock = new THREE.Clock(); // Loader - check if GLTFLoader is available if (typeof THREE.GLTFLoader !== 'undefined') { loader = new THREE.GLTFLoader(); } else { console.warn('GLTFLoader not available, using fallback geometry'); loader = null; } // Add environment map with error handling const envMapLoader = new THREE.CubeTextureLoader(); envMapLoader.load([ 'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/px.jpg', 'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/nx.jpg', 'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/py.jpg', 'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/ny.jpg', 'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/pz.jpg', 'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/nz.jpg' ], (envMap) => { scene.background = envMap; scene.environment = envMap; }, undefined, (error) => { console.warn('Failed to load environment map:', error); // Fallback to plain color background scene.background = new THREE.Color(0x87CEEB); }); // Add better lighting for RPM avatars const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1); scene.add(hemisphereLight); const mainLight = new THREE.DirectionalLight(0xffffff, 0.8); mainLight.position.set(10, 20, 15); mainLight.castShadow = true; mainLight.shadow.mapSize.width = 2048; mainLight.shadow.mapSize.height = 2048; mainLight.shadow.camera.near = 0.5; mainLight.shadow.camera.far = 50; mainLight.shadow.camera.left = -20; mainLight.shadow.camera.right = 20; mainLight.shadow.camera.top = 20; mainLight.shadow.camera.bottom = -20; scene.add(mainLight); // Create ground const groundGeometry = new THREE.PlaneGeometry(100, 100); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x4ade80, roughness: 0.8, metalness: 0.2 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground); // Add some environment objects addEnvironmentObjects(); // Initialize OrbitControls with fallback if (typeof THREE.OrbitControls !== 'undefined') { controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.enabled = false; // Start with controls disabled for follow camera } else { console.warn('OrbitControls not available, using fallback'); controls = { enabled: false, update: () => {}, target: new THREE.Vector3() }; } // Load player character loadPlayerCharacter(); // Load NPC characters loadNPCCharacters(); // Start animation loop animate(); // Handle window resize window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); } catch (error) { console.error('Error initializing Three.js:', error); // Show user-friendly error message alert('Error initializing 3D graphics. Please check your browser supports WebGL.'); } } function addEnvironmentObjects() { // Add some trees const treeGeometry = new THREE.ConeGeometry(1, 3, 8); const treeMaterial = new THREE.MeshStandardMaterial({ color: 0x2e7d32 }); for (let i = 0; i < 20; i++) { const tree = new THREE.Mesh(treeGeometry, treeMaterial); tree.position.x = (Math.random() - 0.5) * 80; tree.position.z = (Math.random() - 0.5) * 80; tree.position.y = 1.5; tree.castShadow = true; scene.add(tree); // Add trunk const trunkGeometry = new THREE.CylinderGeometry(0.3, 0.3, 1); const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x5e4035 }); const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial); trunk.position.y = 0.5; tree.add(trunk); } // Add some rocks const rockGeometry = new THREE.SphereGeometry(0.5, 8, 8); const rockMaterial = new THREE.MeshStandardMaterial({ color: 0x757575 }); for (let i = 0; i < 15; i++) { const rock = new THREE.Mesh(rockGeometry, rockMaterial); rock.position.x = (Math.random() - 0.5) * 80; rock.position.z = (Math.random() - 0.5) * 80; rock.position.y = 0.5; rock.castShadow = true; scene.add(rock); } } function loadPlayerCharacter() { if (loader && typeof THREE.GLTFLoader !== 'undefined') { loader.load(gameState.selectedCharacter.modelUrl, (gltf) => { const model = gltf.scene; // Scale and position the RPM avatar model - adjusted for human proportions model.scale.set(0.8, 0.8, 0.8); model.position.set(0, 0, 0); // Enable shadows for all children model.traverse((child) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }); scene.add(model); // Set up animations if available const mixer = new THREE.AnimationMixer(model); const animations = gltf.animations; gameState.player = { model: model, speed: 0.08, runSpeed: 0.15, rotationSpeed: 0.08, isMoving: false, mixer: mixer, animations: animations, currentAnimation: null, animationActions: {} }; // Set up animation actions if (animations && animations.length > 0) { animations.forEach((clip) => { gameState.player.animationActions[clip.name] = mixer.clipAction(clip); }); // Play the first animation by default if (animations[0]) { gameState.player.animationActions[animations[0].name].play(); gameState.player.currentAnimation = animations[0].name; } } else { // If no animations, create simple placeholder animations createPlayerAnimations(); } }, undefined, (error) => { console.error('Error loading player model:', error); // Fallback to placeholder loadPlayerCharacterFallback(); }); } else { // GLTFLoader not available, use fallback loadPlayerCharacterFallback(); } } function loadPlayerCharacterFallback() { const group = new THREE.Group(); // Body const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 8); const bodyMaterial = new THREE.MeshStandardMaterial({ color: new THREE.Color(gameState.selectedCharacter.color), roughness: 0.7, metalness: 0.1 }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.position.y = 0.75; body.castShadow = true; group.add(body); // Head const headGeometry = new THREE.SphereGeometry(0.4, 8, 8); const headMaterial = new THREE.MeshStandardMaterial({ color: 0xf5d0b5 }); const head = new THREE.Mesh(headGeometry, headMaterial); head.position.y = 1.6; head.castShadow = true; group.add(head); // Arms const armGeometry = new THREE.CylinderGeometry(0.15, 0.15, 0.8, 6); const leftArm = new THREE.Mesh(armGeometry, bodyMaterial); leftArm.position.set(-0.6, 1, 0); leftArm.rotation.z = 0.5; leftArm.castShadow = true; group.add(leftArm); const rightArm = new THREE.Mesh(armGeometry, bodyMaterial); rightArm.position.set(0.6, 1, 0); rightArm.rotation.z = -0.5; rightArm.castShadow = true; group.add(rightArm); // Legs const legGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.8, 6); const leftLeg = new THREE.Mesh(legGeometry, new THREE.MeshStandardMaterial({ color: 0x1e40af })); leftLeg.position.set(-0.2, -0.4, 0); leftLeg.castShadow = true; group.add(leftLeg); const rightLeg = new THREE.Mesh(legGeometry, new THREE.MeshStandardMaterial({ color: 0x1e40af })); rightLeg.position.set(0.2, -0.4, 0); rightLeg.castShadow = true; group.add(rightLeg); group.position.y = 0; scene.add(group); gameState.player = { model: group, speed: 0.08, runSpeed: 0.15, rotationSpeed: 0.08, isMoving: false, animations: { idle: null, walk: null, run: null, jump: null }, currentAnimation: null }; // Add a simple animation mixer for the player mixer = new THREE.AnimationMixer(group); createPlayerAnimations(); setPlayerAnimation('idle'); } function createPlayerAnimations() { // In a real app, these would come from the GLTF model // For this example, we'll create simple animations using NumberKeyframeTrack // Idle animation (slight bounce) const idleTrack = new THREE.NumberKeyframeTrack( '.position[y]', [0, 0.5, 1], [0, 0.1, 0] ); const idleClip = new THREE.AnimationClip('idle', 1, [idleTrack]); gameState.player.animations.idle = idleClip; // Walk animation (arm and leg movement) const leftArmTrack = new THREE.NumberKeyframeTrack( '.children[2].rotation[z]', [0, 0.5, 1], [0.3, -0.3, 0.3] ); const rightArmTrack = new THREE.NumberKeyframeTrack( '.children[3].rotation[z]', [0, 0.5, 1], [-0.3, 0.3, -0.3] ); const leftLegTrack = new THREE.NumberKeyframeTrack( '.children[4].position[y]', [0, 0.5, 1], [-0.4, -0.3, -0.4] ); const rightLegTrack = new THREE.NumberKeyframeTrack( '.children[5].position[y]', [0, 0.5, 1], [-0.3, -0.4, -0.3] ); const walkClip = new THREE.AnimationClip('walk', 0.5, [ leftArmTrack, rightArmTrack, leftLegTrack, rightLegTrack ]); gameState.player.animations.walk = walkClip; // Run animation (faster arm and leg movement) const runClip = new THREE.AnimationClip('run', 0.3, [ leftArmTrack, rightArmTrack, leftLegTrack, rightLegTrack ]); gameState.player.animations.run = runClip; // Jump animation const jumpTrack = new THREE.NumberKeyframeTrack( '.position[y]', [0, 0.2, 0.4, 0.6, 0.8, 1], [0, 1.5, 1, 0.3, 0, 0] ); const jumpClip = new THREE.AnimationClip('jump', 1, [jumpTrack]); gameState.player.animations.jump = jumpClip; } function setPlayerAnimation(name) { if (gameState.player.currentAnimation === name) return; if (mixer) { mixer.stopAllAction(); const clip = gameState.player.animations[name]; if (clip) { const action = mixer.clipAction(clip); action.setLoop(THREE.LoopRepeat); action.clampWhenFinished = false; action.play(); } gameState.player.currentAnimation = name; } } function loadNPCCharacters() { gameState.selectedWorldCharacters.forEach((character, index) => { // In a real app, we would load the GLTF model from the URL // For this example, we'll create a simple placeholder const group = new THREE.Group(); // Body const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 8); const bodyMaterial = new THREE.MeshStandardMaterial({ color: new THREE.Color(character.color), roughness: 0.7, metalness: 0.1 }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.position.y = 0.75; body.castShadow = true; group.add(body); // Head const headGeometry = new THREE.SphereGeometry(0.4, 8, 8); const headMaterial = new THREE.MeshStandardMaterial({ color: 0xf5d0b5 }); const head = new THREE.Mesh(headGeometry, headMaterial); head.position.y = 1.6; head.castShadow = true; group.add(head); // Scale NPCs to match player size group.scale.set(0.8, 0.8, 0.8); // Position NPCs in a circle around the center const angle = (index / gameState.selectedWorldCharacters.length) * Math.PI * 2; const radius = 10 + Math.random() * 10; group.position.x = Math.cos(angle) * radius; group.position.z = Math.sin(angle) * radius; group.position.y = 0; // Make NPC face center group.lookAt(0, 0, 0); scene.add(group); gameState.npcs.push({ model: group, character: character, dialog: character.dialog, currentDialogIndex: 0 }); }); } function animate() { requestAnimationFrame(animate); const delta = Math.min(clock.getDelta(), 0.1); // Cap delta to prevent large jumps // Update player animation mixer if (mixer) { mixer.update(delta); } // Handle player movement handlePlayerMovement(delta); // Check for nearby NPCs checkForNearbyNPCs(); // Update camera based on mode updateCamera(delta); // Update OrbitControls if enabled if (controls && controls.enabled) { controls.update(); } renderer.render(scene, camera); } function handlePlayerMovement(delta) { if (!gameState.player) return; const player = gameState.player; let moving = false; let moveDirection = new THREE.Vector3(); // Check if dialog is open - disable movement if dialog is active const isDialogOpen = !document.getElementById('dialog-box').classList.contains('translate-y-full'); if (isDialogOpen) { // Stop any movement and reset animation if (player.isMoving) { player.isMoving = false; updatePlayerAnimation(); } return; } // Forward/backward movement if (gameState.keys.w) { moveDirection.z = -1; moving = true; } if (gameState.keys.s) { moveDirection.z = 1; moving = true; } // Left/right movement if (gameState.keys.a) { moveDirection.x = -1; moving = true; } if (gameState.keys.d) { moveDirection.x = 1; moving = true; } // Normalize direction vector if moving diagonally if (moving) { moveDirection.normalize(); // Calculate speed with delta time for consistent movement const speed = (gameState.keys.shift ? player.runSpeed : player.speed) * delta * 60; // Apply movement relative to camera direction const cameraForward = new THREE.Vector3(); camera.getWorldDirection(cameraForward); cameraForward.y = 0; cameraForward.normalize(); const cameraRight = new THREE.Vector3(); cameraRight.crossVectors(cameraForward, new THREE.Vector3(0, 1, 0)); cameraRight.normalize(); const moveVector = new THREE.Vector3(); moveVector.addScaledVector(cameraForward, moveDirection.z * speed); moveVector.addScaledVector(cameraRight, moveDirection.x * speed); player.model.position.add(moveVector); // Update player rotation to face movement direction if (moveDirection.length() > 0.1) { const targetRotation = Math.atan2(moveDirection.x, moveDirection.z) + camera.rotation.y; player.model.rotation.y = THREE.MathUtils.lerp(player.model.rotation.y, targetRotation, player.rotationSpeed * delta * 60); } } // Jumping - only trigger when not showing dialog and not already jumping if (gameState.keys.space && !gameState.isJumping && !isDialogOpen) { gameState.isJumping = true; setPlayerAnimation('jump'); // Create a more robust jump animation let jumpStartTime = performance.now(); const jumpDuration = 1000; // 1 second const originalY = player.model.position.y; const jumpHeight = 1.5; function performJump() { const currentTime = performance.now(); const elapsed = currentTime - jumpStartTime; const progress = Math.min(elapsed / jumpDuration, 1); // Parabolic jump curve const jumpProgress = 1 - Math.pow(2 * progress - 1, 2); player.model.position.y = originalY + jumpHeight * jumpProgress; if (progress < 1) { requestAnimationFrame(performJump); } else { gameState.isJumping = false; player.model.position.y = originalY; updatePlayerAnimation(); } } performJump(); } // Update animation based on movement if (moving !== player.isMoving || gameState.keys.shift) { player.isMoving = moving; updatePlayerAnimation(); } // Update camera position based on camera mode updateCamera(delta); } function updateCamera(delta) { if (!gameState.player) return; const player = gameState.player; switch (gameState.cameraMode) { case 'follow': // Follow camera - smooth follow behind player const targetCameraPosition = player.model.position.clone().add(new THREE.Vector3(0, 3, 8)); camera.position.lerp(targetCameraPosition, 0.1 * delta * 60); camera.lookAt(player.model.position); break; case 'orbit': // Orbit camera - let OrbitControls handle positioning // Set the target to player position controls.target.copy(player.model.position); break; case 'first-person': // First-person camera - attach to player's head const headPosition = player.model.position.clone(); headPosition.y += 1.2; // Eye level camera.position.copy(headPosition); // Make camera face same direction as player camera.rotation.y = player.model.rotation.y; break; } } function cycleCameraMode() { const modes = ['follow', 'orbit', 'first-person']; const currentIndex = modes.indexOf(gameState.cameraMode); const nextIndex = (currentIndex + 1) % modes.length; gameState.cameraMode = modes[nextIndex]; // Update UI display document.getElementById('camera-mode-display').textContent = gameState.cameraMode.charAt(0).toUpperCase() + gameState.cameraMode.slice(1); // Enable/disable OrbitControls based on mode if (controls && typeof controls.enabled !== 'undefined') { controls.enabled = (gameState.cameraMode === 'orbit'); } // Reset camera position for first-person mode if (gameState.cameraMode === 'first-person') { camera.rotation.set(0, 0, 0); } } function updatePlayerAnimation() { if (gameState.isJumping) return; if (!gameState.player.isMoving) { setPlayerAnimation('idle'); } else if (gameState.keys.shift) { setPlayerAnimation('run'); } else { setPlayerAnimation('walk'); } } function checkForNearbyNPCs() { if (!gameState.player) return; let closestNpc = null; let closestDistance = Infinity; gameState.npcs.forEach(npc => { const distance = npc.model.position.distanceTo(gameState.player.model.position); if (distance < 3 && distance < closestDistance) { // Reduced distance from 5 to 3 closestDistance = distance; closestNpc = npc; } }); gameState.nearbyNpc = closestNpc; // Add visual feedback when near NPC const dialogBox = document.getElementById('dialog-box'); if (closestNpc && !dialogBox.classList.contains('translate-y-full')) { // Dialog is already open, no need to add feedback } else if (closestNpc) { // Show talk prompt const gameUI = document.getElementById('game-ui'); if (!gameUI.querySelector('.npc-talk-prompt')) { const prompt = document.createElement('div'); prompt.className = 'npc-talk-prompt absolute bottom-20 left-4 bg-yellow-500 text-black px-3 py-2 rounded-lg font-bold'; prompt.textContent = 'Press SPACE to talk'; gameUI.appendChild(prompt); } } else { // Remove talk prompt if no NPC nearby const existingPrompt = document.getElementById('game-ui')?.querySelector('.npc-talk-prompt'); if (existingPrompt) { existingPrompt.remove(); } } } function showDialog(npc) { if (!npc) return; // Get next dialog line const dialog = npc.dialog[npc.currentDialogIndex]; npc.currentDialogIndex = (npc.currentDialogIndex + 1) % npc.dialog.length; // Update dialog UI document.getElementById('dialog-character').style.backgroundColor = npc.character.color; document.getElementById('dialog-name').textContent = npc.character.name; document.getElementById('dialog-text').textContent = dialog; // Show dialog box document.getElementById('dialog-box').classList.remove('translate-y-full'); // Remove the talk prompt when dialog opens const existingPrompt = document.getElementById('game-ui')?.querySelector('.npc-talk-prompt'); if (existingPrompt) { existingPrompt.remove(); } } function closeDialog() { document.getElementById('dialog-box').classList.add('translate-y-full'); // Remove the talk prompt when dialog closes const existingPrompt = document.getElementById('game-ui')?.querySelector('.npc-talk-prompt'); if (existingPrompt) { existingPrompt.remove(); } } function handleKeyDown(event) { // Prevent default for space to avoid page scrolling if (event.key === ' ') { event.preventDefault(); } // Ignore key repeats if (event.repeat) return; const key = event.key.toLowerCase(); switch (key) { case 'w': gameState.keys.w = true; break; case 'a': gameState.keys.a = true; break; case 's': gameState.keys.s = true; break; case 'd': gameState.keys.d = true; break; case 'shift': gameState.keys.shift = true; break; case ' ': gameState.keys.space = true; // Check if we're near an NPC and dialog isn't already open const isDialogOpen = !document.getElementById('dialog-box').classList.contains('translate-y-full'); if (gameState.nearbyNpc && !gameState.isJumping && !isDialogOpen) { showDialog(gameState.nearbyNpc); } break; case 'c': // Camera mode switching is handled separately break; case 'm': // Music toggle is handled separately break; } } function handleKeyUp(event) { // Ignore key repeats if (event.repeat) return; const key = event.key.toLowerCase(); switch (key) { case 'w': gameState.keys.w = false; break; case 'a': gameState.keys.a = false; break; case 's': gameState.keys.s = false; break; case 'd': gameState.keys.d = false; break; case 'shift': gameState.keys.shift = false; break; case ' ': gameState.keys.space = false; break; } // Update animation when keys are released if (gameState.player) { updatePlayerAnimation(); } }

Made with DeepSite LogoDeepSite - 🧬 Remix