Spaces:
Running
Running
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Tesla 3D 무한맵 운전 게임</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| overflow: hidden; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| background: #000; | |
| } | |
| #gameContainer { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| #minimap { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| width: 200px; | |
| height: 200px; | |
| background: rgba(0, 0, 0, 0.8); | |
| border: 2px solid #00ff00; | |
| border-radius: 15px; | |
| z-index: 100; | |
| box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); | |
| } | |
| #speedometer { | |
| position: absolute; | |
| bottom: 30px; | |
| right: 30px; | |
| font-size: 20px; | |
| color: white; | |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); | |
| z-index: 100; | |
| background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.8)); | |
| padding: 20px; | |
| border-radius: 15px; | |
| min-width: 200px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| } | |
| #speedometer .speed-display { | |
| font-size: 48px; | |
| font-weight: 300; | |
| color: #00ff88; | |
| text-align: center; | |
| margin: 10px 0; | |
| } | |
| #controls { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| color: white; | |
| background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.8)); | |
| padding: 20px; | |
| border-radius: 15px; | |
| z-index: 100; | |
| font-size: 14px; | |
| line-height: 1.8; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| } | |
| #score { | |
| position: absolute; | |
| top: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| color: white; | |
| font-size: 24px; | |
| font-weight: 300; | |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); | |
| z-index: 100; | |
| background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.8)); | |
| padding: 15px 30px; | |
| border-radius: 15px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| } | |
| .gear-indicator { | |
| text-align: center; | |
| font-size: 32px; | |
| margin-top: 10px; | |
| font-weight: bold; | |
| } | |
| .gear-p { color: #ff4444; } | |
| .gear-r { color: #ffaa44; } | |
| .gear-n { color: #ffffff; } | |
| .gear-d { color: #00ff88; } | |
| canvas { | |
| display: block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="gameContainer"> | |
| <div id="controls"> | |
| <div><strong>🎮 Tesla 운전 시뮬레이터</strong></div> | |
| <div>W/↑ - 가속</div> | |
| <div>S/↓ - 브레이크/후진</div> | |
| <div>A/← - 좌회전</div> | |
| <div>D/→ - 우회전</div> | |
| <div>Space - 핸드브레이크</div> | |
| <div>C - 카메라 변경</div> | |
| <div>L - 헤드라이트</div> | |
| </div> | |
| <div id="score">주행 거리: 0 km</div> | |
| <canvas id="minimap"></canvas> | |
| <div id="speedometer"> | |
| <div style="text-align: center; color: #888; font-size: 12px;">TESLA</div> | |
| <div class="speed-display"><span id="speed">0</span></div> | |
| <div style="text-align: center; color: #888; font-size: 14px;">km/h</div> | |
| <div class="gear-indicator gear-n" id="gear">P</div> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| // Game variables | |
| let scene, camera, renderer; | |
| let car, carSpeed = 0, carRotation = 0; | |
| let keys = {}; | |
| let buildings = []; | |
| let roads = []; | |
| let props = []; | |
| let trees = []; | |
| let distance = 0; | |
| let cameraMode = 0; | |
| let minimapCanvas, minimapCtx; | |
| let clock = new THREE.Clock(); | |
| let headlightsOn = false; | |
| let leftHeadlight, rightHeadlight; | |
| let wheels = []; | |
| const MAX_SPEED = 3; | |
| const ACCELERATION = 0.08; | |
| const DECELERATION = 0.04; | |
| const TURN_SPEED = 0.04; | |
| const BRAKE_DECELERATION = 0.12; | |
| const WORLD_SIZE = 1000; | |
| const CHUNK_SIZE = 150; | |
| // Initialize game | |
| function init() { | |
| // Scene setup | |
| scene = new THREE.Scene(); | |
| scene.fog = new THREE.Fog(0x8EAEC0, 200, 800); | |
| // Camera setup | |
| camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 2000 | |
| ); | |
| // Renderer setup | |
| renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| renderer.toneMappingExposure = 0.8; | |
| document.getElementById('gameContainer').appendChild(renderer.domElement); | |
| // Sky | |
| createSky(); | |
| // Lighting | |
| setupLighting(); | |
| // Create Tesla car | |
| createTeslaCar(); | |
| // Create initial world | |
| createWorld(); | |
| // Setup minimap | |
| setupMinimap(); | |
| // Event listeners | |
| window.addEventListener('keydown', handleKeyDown); | |
| window.addEventListener('keyup', handleKeyUp); | |
| window.addEventListener('resize', onWindowResize); | |
| // Start game loop | |
| animate(); | |
| } | |
| function createSky() { | |
| // Create gradient sky | |
| const skyGeometry = new THREE.SphereGeometry(1500, 32, 15); | |
| const skyMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| topColor: { value: new THREE.Color(0x0077be) }, | |
| bottomColor: { value: new THREE.Color(0xffffff) }, | |
| offset: { value: 33 }, | |
| exponent: { value: 0.6 } | |
| }, | |
| vertexShader: ` | |
| varying vec3 vWorldPosition; | |
| void main() { | |
| vec4 worldPosition = modelMatrix * vec4(position, 1.0); | |
| vWorldPosition = worldPosition.xyz; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform vec3 topColor; | |
| uniform vec3 bottomColor; | |
| uniform float offset; | |
| uniform float exponent; | |
| varying vec3 vWorldPosition; | |
| void main() { | |
| float h = normalize(vWorldPosition + offset).y; | |
| gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0); | |
| } | |
| `, | |
| side: THREE.BackSide | |
| }); | |
| const sky = new THREE.Mesh(skyGeometry, skyMaterial); | |
| scene.add(sky); | |
| // Add clouds | |
| const cloudGeometry = new THREE.PlaneGeometry(100, 100); | |
| const cloudMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0xffffff, | |
| transparent: true, | |
| opacity: 0.4, | |
| side: THREE.DoubleSide | |
| }); | |
| for(let i = 0; i < 20; i++) { | |
| const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial); | |
| cloud.position.set( | |
| (Math.random() - 0.5) * 1000, | |
| 200 + Math.random() * 200, | |
| (Math.random() - 0.5) * 1000 | |
| ); | |
| cloud.rotation.x = Math.PI / 2; | |
| cloud.scale.setScalar(Math.random() * 2 + 1); | |
| scene.add(cloud); | |
| } | |
| } | |
| function setupLighting() { | |
| // Ambient light | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); | |
| scene.add(ambientLight); | |
| // Sun light | |
| const sunLight = new THREE.DirectionalLight(0xffd4a3, 1); | |
| sunLight.position.set(100, 200, 100); | |
| sunLight.castShadow = true; | |
| sunLight.shadow.camera.left = -150; | |
| sunLight.shadow.camera.right = 150; | |
| sunLight.shadow.camera.top = 150; | |
| sunLight.shadow.camera.bottom = -150; | |
| sunLight.shadow.camera.near = 0.1; | |
| sunLight.shadow.camera.far = 500; | |
| sunLight.shadow.mapSize.width = 2048; | |
| sunLight.shadow.mapSize.height = 2048; | |
| scene.add(sunLight); | |
| // Hemisphere light for better ambient | |
| const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x8B7355, 0.3); | |
| scene.add(hemiLight); | |
| } | |
| function createTeslaCar() { | |
| car = new THREE.Group(); | |
| // Tesla-like car body | |
| const bodyGroup = new THREE.Group(); | |
| // Main body | |
| const bodyGeometry = new THREE.BoxGeometry(2.2, 0.8, 4.8); | |
| const bodyMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x1a1a1a, | |
| metalness: 0.8, | |
| roughness: 0.2 | |
| }); | |
| const carBody = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| carBody.position.y = 0.6; | |
| carBody.castShadow = true; | |
| bodyGroup.add(carBody); | |
| // Sleek roof (Tesla-like curve) | |
| const roofGeometry = new THREE.BoxGeometry(2, 0.6, 3); | |
| const roofMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x0a0a0a, | |
| metalness: 0.9, | |
| roughness: 0.1 | |
| }); | |
| const carRoof = new THREE.Mesh(roofGeometry, roofMaterial); | |
| carRoof.position.y = 1.2; | |
| carRoof.position.z = -0.3; | |
| // Round the edges | |
| carRoof.scale.set(0.95, 1, 1); | |
| bodyGroup.add(carRoof); | |
| // Glass windows | |
| const glassGeometry = new THREE.BoxGeometry(1.9, 0.5, 2.8); | |
| const glassMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x222244, | |
| transparent: true, | |
| opacity: 0.3, | |
| metalness: 0.9, | |
| roughness: 0.1 | |
| }); | |
| const glass = new THREE.Mesh(glassGeometry, glassMaterial); | |
| glass.position.y = 1.2; | |
| glass.position.z = -0.3; | |
| bodyGroup.add(glass); | |
| // Front hood | |
| const hoodGeometry = new THREE.BoxGeometry(2.1, 0.2, 1.5); | |
| const hood = new THREE.Mesh(hoodGeometry, bodyMaterial); | |
| hood.position.y = 0.8; | |
| hood.position.z = 2.2; | |
| hood.rotation.x = -0.1; | |
| bodyGroup.add(hood); | |
| // Wheels - Tesla style | |
| const wheelGeometry = new THREE.CylinderGeometry(0.35, 0.35, 0.25, 32); | |
| const wheelMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x1a1a1a, | |
| metalness: 0.8, | |
| roughness: 0.3 | |
| }); | |
| // Wheel rims | |
| const rimGeometry = new THREE.CylinderGeometry(0.32, 0.32, 0.26, 8); | |
| const rimMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x888888, | |
| metalness: 0.9, | |
| roughness: 0.2 | |
| }); | |
| const wheelPositions = [ | |
| { x: -0.95, y: 0.1, z: 1.6 }, | |
| { x: 0.95, y: 0.1, z: 1.6 }, | |
| { x: -0.95, y: 0.1, z: -1.6 }, | |
| { x: 0.95, y: 0.1, z: -1.6 } | |
| ]; | |
| wheelPositions.forEach(pos => { | |
| const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); | |
| wheel.rotation.z = Math.PI / 2; | |
| wheel.position.set(pos.x, pos.y, pos.z); | |
| wheel.castShadow = true; | |
| bodyGroup.add(wheel); | |
| wheels.push(wheel); | |
| const rim = new THREE.Mesh(rimGeometry, rimMaterial); | |
| rim.rotation.z = Math.PI / 2; | |
| rim.position.set(pos.x * 1.01, pos.y, pos.z); | |
| bodyGroup.add(rim); | |
| }); | |
| // Headlights - Tesla style LED | |
| const headlightGeometry = new THREE.BoxGeometry(0.4, 0.2, 0.1); | |
| const headlightMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xffffff, | |
| emissive: 0xffffff, | |
| emissiveIntensity: 0.2 | |
| }); | |
| const headlight1 = new THREE.Mesh(headlightGeometry, headlightMaterial); | |
| headlight1.position.set(-0.7, 0.6, 2.4); | |
| bodyGroup.add(headlight1); | |
| const headlight2 = new THREE.Mesh(headlightGeometry, headlightMaterial); | |
| headlight2.position.set(0.7, 0.6, 2.4); | |
| bodyGroup.add(headlight2); | |
| // LED tail lights | |
| const taillightGeometry = new THREE.BoxGeometry(0.5, 0.15, 0.1); | |
| const taillightMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xff0000, | |
| emissive: 0xff0000, | |
| emissiveIntensity: 0.3 | |
| }); | |
| const taillight1 = new THREE.Mesh(taillightGeometry, taillightMaterial); | |
| taillight1.position.set(-0.8, 0.7, -2.4); | |
| bodyGroup.add(taillight1); | |
| const taillight2 = new THREE.Mesh(taillightGeometry, taillightMaterial); | |
| taillight2.position.set(0.8, 0.7, -2.4); | |
| bodyGroup.add(taillight2); | |
| // Door handles (flush like Tesla) | |
| const handleGeometry = new THREE.BoxGeometry(0.02, 0.1, 0.4); | |
| const handleMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x444444, | |
| metalness: 0.9 | |
| }); | |
| [-1.1, 1.1].forEach(x => { | |
| [-0.5, 0.8].forEach(z => { | |
| const handle = new THREE.Mesh(handleGeometry, handleMaterial); | |
| handle.position.set(x, 0.8, z); | |
| bodyGroup.add(handle); | |
| }); | |
| }); | |
| // Tesla logo/emblem | |
| const logoGeometry = new THREE.PlaneGeometry(0.3, 0.3); | |
| const logoMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xff0000, | |
| emissive: 0xff0000, | |
| emissiveIntensity: 0.2 | |
| }); | |
| const logo = new THREE.Mesh(logoGeometry, logoMaterial); | |
| logo.position.set(0, 0.8, 2.41); | |
| bodyGroup.add(logo); | |
| car.add(bodyGroup); | |
| // Add headlight spotlights | |
| leftHeadlight = new THREE.SpotLight(0xffffff, 0, 50, Math.PI / 4, 0.5, 2); | |
| leftHeadlight.position.set(-0.7, 0.6, 2.4); | |
| leftHeadlight.target.position.set(-0.7, 0, 10); | |
| car.add(leftHeadlight); | |
| car.add(leftHeadlight.target); | |
| rightHeadlight = new THREE.SpotLight(0xffffff, 0, 50, Math.PI / 4, 0.5, 2); | |
| rightHeadlight.position.set(0.7, 0.6, 2.4); | |
| rightHeadlight.target.position.set(0.7, 0, 10); | |
| car.add(rightHeadlight); | |
| car.add(rightHeadlight.target); | |
| car.position.y = 0.3; | |
| scene.add(car); | |
| } | |
| function createWorld() { | |
| // Better ground texture | |
| const groundGeometry = new THREE.PlaneGeometry(WORLD_SIZE * 4, WORLD_SIZE * 4); | |
| const groundMaterial = new THREE.MeshLambertMaterial({ | |
| color: 0x4a5d3a | |
| }); | |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.position.y = -0.1; | |
| ground.receiveShadow = true; | |
| scene.add(ground); | |
| // Create initial city blocks | |
| for (let x = -WORLD_SIZE/2; x < WORLD_SIZE/2; x += CHUNK_SIZE) { | |
| for (let z = -WORLD_SIZE/2; z < WORLD_SIZE/2; z += CHUNK_SIZE) { | |
| createCityBlock(x, z); | |
| } | |
| } | |
| } | |
| function createCityBlock(centerX, centerZ) { | |
| // Asphalt roads with better textures | |
| const roadWidth = 12; | |
| const roadMaterial = new THREE.MeshLambertMaterial({ | |
| color: 0x2a2a2a | |
| }); | |
| // Main roads | |
| const hRoadGeometry = new THREE.PlaneGeometry(CHUNK_SIZE, roadWidth); | |
| const hRoad = new THREE.Mesh(hRoadGeometry, roadMaterial); | |
| hRoad.rotation.x = -Math.PI / 2; | |
| hRoad.position.set(centerX, 0.01, centerZ); | |
| hRoad.receiveShadow = true; | |
| scene.add(hRoad); | |
| roads.push(hRoad); | |
| const vRoadGeometry = new THREE.PlaneGeometry(roadWidth, CHUNK_SIZE); | |
| const vRoad = new THREE.Mesh(vRoadGeometry, roadMaterial); | |
| vRoad.rotation.x = -Math.PI / 2; | |
| vRoad.position.set(centerX, 0.01, centerZ); | |
| vRoad.receiveShadow = true; | |
| scene.add(vRoad); | |
| roads.push(vRoad); | |
| // Road lines | |
| const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); | |
| for(let i = -CHUNK_SIZE/2; i < CHUNK_SIZE/2; i += 10) { | |
| const lineGeometry = new THREE.PlaneGeometry(0.2, 4); | |
| const line = new THREE.Mesh(lineGeometry, lineMaterial); | |
| line.rotation.x = -Math.PI / 2; | |
| line.position.set(centerX, 0.02, centerZ + i); | |
| scene.add(line); | |
| } | |
| // Sidewalks | |
| const sidewalkMaterial = new THREE.MeshLambertMaterial({ color: 0x808080 }); | |
| const sidewalkGeometry = new THREE.BoxGeometry(CHUNK_SIZE, 0.2, 2); | |
| [-7, 7].forEach(offset => { | |
| const sidewalk = new THREE.Mesh(sidewalkGeometry, sidewalkMaterial); | |
| sidewalk.position.set(centerX, 0.1, centerZ + offset); | |
| scene.add(sidewalk); | |
| }); | |
| // Buildings in quadrants | |
| const quadrants = [ | |
| { x: centerX - CHUNK_SIZE/3, z: centerZ - CHUNK_SIZE/3 }, | |
| { x: centerX + CHUNK_SIZE/3, z: centerZ - CHUNK_SIZE/3 }, | |
| { x: centerX - CHUNK_SIZE/3, z: centerZ + CHUNK_SIZE/3 }, | |
| { x: centerX + CHUNK_SIZE/3, z: centerZ + CHUNK_SIZE/3 } | |
| ]; | |
| quadrants.forEach(quad => { | |
| const rand = Math.random(); | |
| if (rand > 0.3) { | |
| createModernBuildings(quad.x, quad.z, CHUNK_SIZE/4); | |
| } else { | |
| createPark(quad.x, quad.z, CHUNK_SIZE/4); | |
| } | |
| }); | |
| // Street furniture | |
| createStreetFurniture(centerX, centerZ); | |
| } | |
| function createModernBuildings(centerX, centerZ, size) { | |
| const buildingCount = Math.floor(Math.random() * 3) + 2; | |
| for (let i = 0; i < buildingCount; i++) { | |
| const width = Math.random() * 20 + 15; | |
| const depth = Math.random() * 20 + 15; | |
| const height = Math.random() * 60 + 30; | |
| const floors = Math.floor(height / 3); | |
| const buildingGroup = new THREE.Group(); | |
| // Building base | |
| const buildingGeometry = new THREE.BoxGeometry(width, height, depth); | |
| const buildingColor = new THREE.Color( | |
| 0.3 + Math.random() * 0.3, | |
| 0.3 + Math.random() * 0.3, | |
| 0.3 + Math.random() * 0.4 | |
| ); | |
| const buildingMaterial = new THREE.MeshPhongMaterial({ | |
| color: buildingColor, | |
| metalness: 0.5, | |
| roughness: 0.5 | |
| }); | |
| const building = new THREE.Mesh(buildingGeometry, buildingMaterial); | |
| building.position.y = height / 2; | |
| building.castShadow = true; | |
| building.receiveShadow = true; | |
| buildingGroup.add(building); | |
| // Windows | |
| for(let floor = 0; floor < floors; floor++) { | |
| for(let side = 0; side < 4; side++) { | |
| const windowCount = Math.floor(width / 3); | |
| for(let w = 0; w < windowCount; w++) { | |
| const windowGeometry = new THREE.BoxGeometry(1.5, 2, 0.1); | |
| const windowMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x87CEEB, | |
| emissive: 0x444466, | |
| emissiveIntensity: 0.3, | |
| metalness: 0.9, | |
| roughness: 0.1 | |
| }); | |
| const window = new THREE.Mesh(windowGeometry, windowMaterial); | |
| const angle = (side * Math.PI) / 2; | |
| const radius = (side % 2 === 0) ? depth/2 : width/2; | |
| window.position.set( | |
| Math.sin(angle) * radius * 1.01, | |
| floor * 3 + 2, | |
| Math.cos(angle) * radius * 1.01 | |
| ); | |
| window.rotation.y = angle; | |
| if(side < 2) { | |
| window.position.x += (w - windowCount/2) * 2.5; | |
| } else { | |
| window.position.z += (w - windowCount/2) * 2.5; | |
| } | |
| buildingGroup.add(window); | |
| } | |
| } | |
| } | |
| buildingGroup.position.set( | |
| centerX + (Math.random() - 0.5) * size, | |
| 0, | |
| centerZ + (Math.random() - 0.5) * size | |
| ); | |
| scene.add(buildingGroup); | |
| buildings.push(buildingGroup); | |
| } | |
| } | |
| function createPark(centerX, centerZ, size) { | |
| // Grass ground | |
| const parkGeometry = new THREE.PlaneGeometry(size, size); | |
| const parkMaterial = new THREE.MeshLambertMaterial({ | |
| color: 0x3a7d3a | |
| }); | |
| const park = new THREE.Mesh(parkGeometry, parkMaterial); | |
| park.rotation.x = -Math.PI / 2; | |
| park.position.set(centerX, 0.02, centerZ); | |
| park.receiveShadow = true; | |
| scene.add(park); | |
| // Walking paths | |
| const pathMaterial = new THREE.MeshLambertMaterial({ color: 0x8B7355 }); | |
| const pathGeometry = new THREE.PlaneGeometry(2, size); | |
| const path1 = new THREE.Mesh(pathGeometry, pathMaterial); | |
| path1.rotation.x = -Math.PI / 2; | |
| path1.position.set(centerX, 0.03, centerZ); | |
| scene.add(path1); | |
| const path2 = new THREE.Mesh(new THREE.PlaneGeometry(size, 2), pathMaterial); | |
| path2.rotation.x = -Math.PI / 2; | |
| path2.position.set(centerX, 0.03, centerZ); | |
| scene.add(path2); | |
| // Trees | |
| const treeCount = Math.floor(Math.random() * 8) + 5; | |
| for (let i = 0; i < treeCount; i++) { | |
| const x = centerX + (Math.random() - 0.5) * size * 0.8; | |
| const z = centerZ + (Math.random() - 0.5) * size * 0.8; | |
| // Avoid paths | |
| if(Math.abs(x - centerX) > 2 && Math.abs(z - centerZ) > 2) { | |
| createRealisticTree(x, z); | |
| } | |
| } | |
| // Park benches | |
| for(let i = 0; i < 3; i++) { | |
| createBench( | |
| centerX + (Math.random() - 0.5) * size * 0.6, | |
| centerZ + (Math.random() - 0.5) * size * 0.6 | |
| ); | |
| } | |
| } | |
| function createRealisticTree(x, z) { | |
| const tree = new THREE.Group(); | |
| // Trunk | |
| const trunkGeometry = new THREE.CylinderGeometry(0.8, 1, 6, 8); | |
| const trunkMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x4a3c28, | |
| roughness: 0.8 | |
| }); | |
| const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial); | |
| trunk.position.y = 3; | |
| trunk.castShadow = true; | |
| tree.add(trunk); | |
| // Foliage layers | |
| const foliageColors = [0x2d5a2d, 0x3a6b3a, 0x4a7c4a]; | |
| const foliageSizes = [5, 4, 3]; | |
| const foliageHeights = [6, 8, 10]; | |
| foliageColors.forEach((color, i) => { | |
| const foliageGeometry = new THREE.SphereGeometry(foliageSizes[i], 8, 6); | |
| const foliageMaterial = new THREE.MeshPhongMaterial({ | |
| color: color, | |
| roughness: 0.8 | |
| }); | |
| const foliage = new THREE.Mesh(foliageGeometry, foliageMaterial); | |
| foliage.position.y = foliageHeights[i]; | |
| foliage.castShadow = true; | |
| tree.add(foliage); | |
| }); | |
| tree.position.set(x, 0, z); | |
| scene.add(tree); | |
| trees.push(tree); | |
| } | |
| function createStreetFurniture(centerX, centerZ) { | |
| // Modern street lights | |
| const positions = [ | |
| { x: centerX - 20, z: centerZ - 20 }, | |
| { x: centerX + 20, z: centerZ - 20 }, | |
| { x: centerX - 20, z: centerZ + 20 }, | |
| { x: centerX + 20, z: centerZ + 20 } | |
| ]; | |
| positions.forEach(pos => { | |
| const pole = new THREE.Group(); | |
| // Pole | |
| const poleGeometry = new THREE.CylinderGeometry(0.15, 0.2, 10); | |
| const poleMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x333333, | |
| metalness: 0.8, | |
| roughness: 0.3 | |
| }); | |
| const poleMesh = new THREE.Mesh(poleGeometry, poleMaterial); | |
| poleMesh.position.y = 5; | |
| poleMesh.castShadow = true; | |
| pole.add(poleMesh); | |
| // LED Light fixture | |
| const lightGeometry = new THREE.BoxGeometry(2, 0.3, 0.8); | |
| const lightMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xffffff, | |
| emissive: 0xffffaa, | |
| emissiveIntensity: 0.5 | |
| }); | |
| const lightMesh = new THREE.Mesh(lightGeometry, lightMaterial); | |
| lightMesh.position.y = 10; | |
| pole.add(lightMesh); | |
| // Add actual light | |
| const streetLight = new THREE.PointLight(0xffffaa, 0.5, 30); | |
| streetLight.position.y = 9.5; | |
| pole.add(streetLight); | |
| pole.position.set(pos.x, 0, pos.z); | |
| scene.add(pole); | |
| props.push(pole); | |
| }); | |
| // Traffic lights | |
| if(Math.random() > 0.5) { | |
| createTrafficLight(centerX + 8, centerZ + 8); | |
| } | |
| // Fire hydrants | |
| if(Math.random() > 0.6) { | |
| createFireHydrant(centerX + 15, centerZ - 15); | |
| } | |
| } | |
| function createTrafficLight(x, z) { | |
| const trafficLight = new THREE.Group(); | |
| // Pole | |
| const poleGeometry = new THREE.CylinderGeometry(0.1, 0.1, 8); | |
| const poleMaterial = new THREE.MeshPhongMaterial({ color: 0x333333 }); | |
| const pole = new THREE.Mesh(poleGeometry, poleMaterial); | |
| pole.position.y = 4; | |
| trafficLight.add(pole); | |
| // Light box | |
| const boxGeometry = new THREE.BoxGeometry(0.8, 2.4, 0.8); | |
| const boxMaterial = new THREE.MeshPhongMaterial({ color: 0x1a1a1a }); | |
| const box = new THREE.Mesh(boxGeometry, boxMaterial); | |
| box.position.y = 8; | |
| trafficLight.add(box); | |
| // Lights | |
| const colors = [0xff0000, 0xffff00, 0x00ff00]; | |
| colors.forEach((color, i) => { | |
| const lightGeometry = new THREE.SphereGeometry(0.3); | |
| const lightMaterial = new THREE.MeshPhongMaterial({ | |
| color: color, | |
| emissive: color, | |
| emissiveIntensity: i === 2 ? 0.8 : 0.1 | |
| }); | |
| const light = new THREE.Mesh(lightGeometry, lightMaterial); | |
| light.position.y = 8.8 - i * 0.8; | |
| light.position.z = 0.41; | |
| trafficLight.add(light); | |
| }); | |
| trafficLight.position.set(x, 0, z); | |
| scene.add(trafficLight); | |
| props.push(trafficLight); | |
| } | |
| function createFireHydrant(x, z) { | |
| const hydrant = new THREE.Group(); | |
| const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.35, 1.5); | |
| const bodyMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xff0000, | |
| metalness: 0.5, | |
| roughness: 0.4 | |
| }); | |
| const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
| body.position.y = 0.75; | |
| hydrant.add(body); | |
| const topGeometry = new THREE.SphereGeometry(0.35, 8, 4); | |
| const top = new THREE.Mesh(topGeometry, bodyMaterial); | |
| top.position.y = 1.5; | |
| hydrant.add(top); | |
| hydrant.position.set(x, 0, z); | |
| hydrant.castShadow = true; | |
| scene.add(hydrant); | |
| props.push(hydrant); | |
| } | |
| function createBench(x, z) { | |
| const bench = new THREE.Group(); | |
| // Seat | |
| const seatGeometry = new THREE.BoxGeometry(4, 0.2, 1.5); | |
| const woodMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x8B4513, | |
| roughness: 0.8 | |
| }); | |
| const seat = new THREE.Mesh(seatGeometry, woodMaterial); | |
| seat.position.y = 0.6; | |
| bench.add(seat); | |
| // Back | |
| const backGeometry = new THREE.BoxGeometry(4, 1.2, 0.2); | |
| const back = new THREE.Mesh(backGeometry, woodMaterial); | |
| back.position.y = 1.2; | |
| back.position.z = -0.6; | |
| back.rotation.x = -0.1; | |
| bench.add(back); | |
| // Metal legs | |
| const legMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x444444, | |
| metalness: 0.8 | |
| }); | |
| const legGeometry = new THREE.CylinderGeometry(0.05, 0.05, 0.6); | |
| [-1.8, 1.8].forEach(xOffset => { | |
| [-0.5, 0.5].forEach(zOffset => { | |
| const leg = new THREE.Mesh(legGeometry, legMaterial); | |
| leg.position.set(xOffset, 0.3, zOffset); | |
| bench.add(leg); | |
| }); | |
| }); | |
| bench.position.set(x, 0, z); | |
| bench.rotation.y = Math.random() * Math.PI; | |
| bench.castShadow = true; | |
| scene.add(bench); | |
| props.push(bench); | |
| } | |
| function setupMinimap() { | |
| minimapCanvas = document.getElementById('minimap'); | |
| minimapCanvas.width = 200; | |
| minimapCanvas.height = 200; | |
| minimapCtx = minimapCanvas.getContext('2d'); | |
| } | |
| function updateMinimap() { | |
| // Clear | |
| minimapCtx.fillStyle = 'rgba(0, 0, 0, 0.9)'; | |
| minimapCtx.fillRect(0, 0, 200, 200); | |
| // Draw grid | |
| minimapCtx.strokeStyle = '#2a2a2a'; | |
| minimapCtx.lineWidth = 1; | |
| const scale = 0.4; | |
| const offsetX = 100 - car.position.x * scale; | |
| const offsetZ = 100 - car.position.z * scale; | |
| // Grid lines | |
| for (let x = -WORLD_SIZE; x <= WORLD_SIZE; x += CHUNK_SIZE) { | |
| minimapCtx.beginPath(); | |
| minimapCtx.moveTo(x * scale + offsetX, 0); | |
| minimapCtx.lineTo(x * scale + offsetX, 200); | |
| minimapCtx.stroke(); | |
| } | |
| for (let z = -WORLD_SIZE; z <= WORLD_SIZE; z += CHUNK_SIZE) { | |
| minimapCtx.beginPath(); | |
| minimapCtx.moveTo(0, z * scale + offsetZ); | |
| minimapCtx.lineTo(200, z * scale + offsetZ); | |
| minimapCtx.stroke(); | |
| } | |
| // Draw buildings | |
| buildings.forEach(building => { | |
| const bx = building.position.x * scale + offsetX; | |
| const bz = building.position.z * scale + offsetZ; | |
| if (bx > -20 && bx < 220 && bz > -20 && bz < 220) { | |
| minimapCtx.fillStyle = '#444'; | |
| minimapCtx.fillRect(bx - 4, bz - 4, 8, 8); | |
| } | |
| }); | |
| // Draw trees | |
| trees.forEach(tree => { | |
| const tx = tree.position.x * scale + offsetX; | |
| const tz = tree.position.z * scale + offsetZ; | |
| if (tx > -10 && tx < 210 && tz > -10 && tz < 210) { | |
| minimapCtx.fillStyle = '#2a5a2a'; | |
| minimapCtx.beginPath(); | |
| minimapCtx.arc(tx, tz, 2, 0, Math.PI * 2); | |
| minimapCtx.fill(); | |
| } | |
| }); | |
| // Draw car | |
| minimapCtx.save(); | |
| minimapCtx.translate(100, 100); | |
| minimapCtx.rotate(-car.rotation.y); | |
| // Car body | |
| minimapCtx.fillStyle = '#00ff88'; | |
| minimapCtx.fillRect(-4, -7, 8, 14); | |
| // Direction indicator | |
| minimapCtx.strokeStyle = '#00ffff'; | |
| minimapCtx.lineWidth = 2; | |
| minimapCtx.beginPath(); | |
| minimapCtx.moveTo(0, 0); | |
| minimapCtx.lineTo(0, -15); | |
| minimapCtx.stroke(); | |
| minimapCtx.restore(); | |
| } | |
| function handleKeyDown(e) { | |
| keys[e.key.toLowerCase()] = true; | |
| if (e.key.toLowerCase() === 'l') { | |
| headlightsOn = !headlightsOn; | |
| leftHeadlight.intensity = headlightsOn ? 2 : 0; | |
| rightHeadlight.intensity = headlightsOn ? 2 : 0; | |
| } | |
| } | |
| function handleKeyUp(e) { | |
| keys[e.key.toLowerCase()] = false; | |
| } | |
| function updateCar() { | |
| const delta = clock.getDelta(); | |
| // Handle input | |
| if (keys['w'] || keys['arrowup']) { | |
| carSpeed = Math.min(carSpeed + ACCELERATION, MAX_SPEED); | |
| } else if (keys['s'] || keys['arrowdown']) { | |
| if (carSpeed > 0) { | |
| carSpeed = Math.max(carSpeed - BRAKE_DECELERATION, 0); | |
| } else { | |
| carSpeed = Math.max(carSpeed - ACCELERATION, -MAX_SPEED / 2); | |
| } | |
| } else { | |
| if (carSpeed > 0) { | |
| carSpeed = Math.max(carSpeed - DECELERATION, 0); | |
| } else if (carSpeed < 0) { | |
| carSpeed = Math.min(carSpeed + DECELERATION, 0); | |
| } | |
| } | |
| // Turning | |
| if (Math.abs(carSpeed) > 0.01) { | |
| let turnAmount = TURN_SPEED; | |
| if(Math.abs(carSpeed) < 0.5) turnAmount *= Math.abs(carSpeed) * 2; | |
| if (keys['a'] || keys['arrowleft']) { | |
| carRotation += turnAmount * (carSpeed > 0 ? 1 : -1); | |
| } | |
| if (keys['d'] || keys['arrowright']) { | |
| carRotation -= turnAmount * (carSpeed > 0 ? 1 : -1); | |
| } | |
| } | |
| // Handbrake | |
| if (keys[' '] && Math.abs(carSpeed) > 0.5) { | |
| carSpeed *= 0.92; | |
| if (keys['a'] || keys['arrowleft']) { | |
| carRotation += TURN_SPEED * 2; | |
| } | |
| if (keys['d'] || keys['arrowright']) { | |
| carRotation -= TURN_SPEED * 2; | |
| } | |
| } | |
| // Update position | |
| car.rotation.y = carRotation; | |
| car.position.x += Math.sin(carRotation) * carSpeed; | |
| car.position.z += Math.cos(carRotation) * carSpeed; | |
| // Rotate wheels | |
| wheels.forEach(wheel => { | |
| wheel.rotation.x += carSpeed * 0.5; | |
| }); | |
| // Update camera | |
| updateCamera(); | |
| // Generate world | |
| checkWorldGeneration(); | |
| // Update UI | |
| updateUI(); | |
| // Update distance | |
| distance += Math.abs(carSpeed) * 0.05; | |
| document.getElementById('score').textContent = `주행 거리: ${distance.toFixed(1)} km`; | |
| } | |
| function updateCamera() { | |
| if (keys['c']) { | |
| keys['c'] = false; | |
| cameraMode = (cameraMode + 1) % 3; | |
| } | |
| const smoothness = 0.1; | |
| let targetX, targetY, targetZ; | |
| let lookX, lookY, lookZ; | |
| switch(cameraMode) { | |
| case 0: // Third person | |
| targetX = car.position.x - Math.sin(carRotation) * 20; | |
| targetY = car.position.y + 10; | |
| targetZ = car.position.z - Math.cos(carRotation) * 20; | |
| lookX = car.position.x; | |
| lookY = car.position.y + 2; | |
| lookZ = car.position.z; | |
| break; | |
| case 1: // Hood cam | |
| targetX = car.position.x + Math.sin(carRotation) * 2; | |
| targetY = car.position.y + 3; | |
| targetZ = car.position.z + Math.cos(carRotation) * 2; | |
| lookX = car.position.x + Math.sin(carRotation) * 20; | |
| lookY = car.position.y + 2; | |
| lookZ = car.position.z + Math.cos(carRotation) * 20; | |
| break; | |
| case 2: // Drone view | |
| targetX = car.position.x; | |
| targetY = car.position.y + 50; | |
| targetZ = car.position.z - 10; | |
| lookX = car.position.x; | |
| lookY = car.position.y; | |
| lookZ = car.position.z; | |
| break; | |
| } | |
| camera.position.x += (targetX - camera.position.x) * smoothness; | |
| camera.position.y += (targetY - camera.position.y) * smoothness; | |
| camera.position.z += (targetZ - camera.position.z) * smoothness; | |
| camera.lookAt(lookX, lookY, lookZ); | |
| } | |
| function checkWorldGeneration() { | |
| const carChunkX = Math.floor(car.position.x / CHUNK_SIZE) * CHUNK_SIZE; | |
| const carChunkZ = Math.floor(car.position.z / CHUNK_SIZE) * CHUNK_SIZE; | |
| // Generate new chunks | |
| for (let x = carChunkX - CHUNK_SIZE * 3; x <= carChunkX + CHUNK_SIZE * 3; x += CHUNK_SIZE) { | |
| for (let z = carChunkZ - CHUNK_SIZE * 3; z <= carChunkZ + CHUNK_SIZE * 3; z += CHUNK_SIZE) { | |
| const chunkKey = `${x}_${z}`; | |
| if (!window.generatedChunks) window.generatedChunks = new Set(); | |
| if (!window.generatedChunks.has(chunkKey)) { | |
| createCityBlock(x, z); | |
| window.generatedChunks.add(chunkKey); | |
| } | |
| } | |
| } | |
| // Remove far objects | |
| const maxDistance = CHUNK_SIZE * 4; | |
| [...buildings, ...props, ...roads, ...trees].forEach(obj => { | |
| if (obj.position) { | |
| const distance = obj.position.distanceTo(car.position); | |
| if (distance > maxDistance) { | |
| scene.remove(obj); | |
| buildings = buildings.filter(b => b !== obj); | |
| props = props.filter(p => p !== obj); | |
| roads = roads.filter(r => r !== obj); | |
| trees = trees.filter(t => t !== obj); | |
| } | |
| } | |
| }); | |
| } | |
| function updateUI() { | |
| const speedKmh = Math.floor(Math.abs(carSpeed) * 60); | |
| document.getElementById('speed').textContent = speedKmh; | |
| const gearElement = document.getElementById('gear'); | |
| let gear = 'P'; | |
| let gearClass = 'gear-p'; | |
| if (Math.abs(carSpeed) < 0.01) { | |
| gear = 'P'; | |
| gearClass = 'gear-p'; | |
| } else if (carSpeed > 0.01) { | |
| gear = 'D'; | |
| gearClass = 'gear-d'; | |
| } else if (carSpeed < -0.01) { | |
| gear = 'R'; | |
| gearClass = 'gear-r'; | |
| } | |
| gearElement.textContent = gear; | |
| gearElement.className = `gear-indicator ${gearClass}`; | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| updateCar(); | |
| updateMinimap(); | |
| renderer.render(scene, camera); | |
| } | |
| // Start the game | |
| init(); | |
| </script> | |
| </body> | |
| </html> |