Spaces:
Running
Running
<html> | |
<head> | |
<title>First Person Terrain Walker with Sky</title> | |
<style> | |
body { margin: 0; padding: 0; background: black; } | |
canvas { display: block; } | |
#instructions { | |
position: fixed; | |
top: 12px; | |
left: 12px; | |
background: rgba(0,0,0,0.4); | |
color: white; | |
padding: 10px; | |
font-family: Arial, sans-serif; | |
border-radius: 8px; | |
} | |
#downloadTexture { | |
display: block; | |
margin-top: 10px; | |
padding: 5px 10px; | |
background: #4CAF50; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
font-family: Arial, sans-serif; | |
} | |
#downloadTexture:hover { | |
background: #45a049; | |
} | |
#downloadTexture:disabled { | |
background: #cccccc; | |
cursor: not-allowed; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="instructions"> | |
WASD or Arrow Keys - Move<br> | |
Space - Jump<br> | |
Mouse - Look around<br> | |
Click to start<br> | |
<button id="downloadTexture" disabled>Download Texture</button> | |
</div> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.module.min.js", | |
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/", | |
"three/examples/": "https://unpkg.com/[email protected]/examples/jsm/" | |
} | |
} | |
</script> | |
<script type="module"> | |
import * as THREE from 'three'; | |
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; | |
import { PointerLockControls } from 'three/examples/controls/PointerLockControls.js'; | |
import { Sky } from 'three/addons/objects/Sky.js'; | |
import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js'; | |
const USE_SPARSE = true; | |
// Scene setup | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.shadowMap.enabled = true; | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
renderer.toneMappingExposure = 0.5; | |
document.body.appendChild(renderer.domElement); | |
// Weapon sway parameters | |
let currentWeaponSway = { x: 0, y: 0, z: 0 }; | |
let targetWeaponSway = { x: 0, y: 0, z: 0 }; | |
const maxSwayAmount = 0.03; | |
const swaySpeed = 0.1; | |
const swayLerpFactor = 0.1; | |
let lastSwayUpdate = 0; | |
const swayUpdateInterval = 150; // milliseconds | |
// Download button setup | |
const downloadButton = document.getElementById('downloadTexture'); | |
let extractedTexture = null; | |
// Function to extract and download texture | |
function extractAndSaveTexture(mesh) { | |
if (!mesh.material || !mesh.material.map) { | |
console.warn('No texture found on this mesh'); | |
return null; | |
} | |
const texture = mesh.material.map; | |
const canvas = document.createElement('canvas'); | |
canvas.width = texture.image.width; | |
canvas.height = texture.image.height; | |
const ctx = canvas.getContext('2d'); | |
ctx.drawImage(texture.image, 0, 0); | |
return canvas.toDataURL('image/png'); | |
} | |
// Download button click handler | |
downloadButton.addEventListener('click', () => { | |
if (extractedTexture) { | |
const link = document.createElement('a'); | |
link.href = extractedTexture; | |
link.download = 'terrain_texture.png'; | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
} | |
}); | |
// Sky setup | |
const sky = new Sky(); | |
sky.scale.setScalar(450000); | |
scene.add(sky); | |
const skyUniforms = sky.material.uniforms; | |
skyUniforms['turbidity'].value = 10; | |
skyUniforms['rayleigh'].value = 3; | |
skyUniforms['mieCoefficient'].value = 0.005; | |
skyUniforms['mieDirectionalG'].value = 0.7; | |
const sun = new THREE.Vector3(); | |
const phi = THREE.MathUtils.degToRad(90 - 2); | |
const theta = THREE.MathUtils.degToRad(180); | |
sun.setFromSphericalCoords(1, phi, theta); | |
skyUniforms['sunPosition'].value.copy(sun); | |
// Lighting Setup | |
const ambientLight = new THREE.AmbientLight(0xfffdfd, 0.6); | |
scene.add(ambientLight); | |
const sunLight = new THREE.DirectionalLight(0xfffdfd, USE_SPARSE ? 0.9 : 3.0); | |
sunLight.position.set(50, 500, 50); | |
sunLight.castShadow = true; | |
sunLight.shadow.mapSize.width = 4096; | |
sunLight.shadow.mapSize.height = 4096; | |
sunLight.shadow.camera.near = 0.1; | |
sunLight.shadow.camera.far = 2000; | |
sunLight.shadow.camera.left = -500; | |
sunLight.shadow.camera.right = 500; | |
sunLight.shadow.camera.top = 500; | |
sunLight.shadow.camera.bottom = -500; | |
sunLight.shadow.bias = -0.0001; | |
scene.add(sunLight); | |
// Add Lensflare | |
const textureLoader = new THREE.TextureLoader(); | |
const textureFlare0 = textureLoader.load('/public/textures/lensflare/lensflare0_alpha.png'); | |
const textureFlare1 = textureLoader.load('/public/textures/lensflare/lensflare3.png'); | |
const textureFlare2 = textureLoader.load('/public/textures/lensflare/lensflare3.png'); | |
const lensflare = new Lensflare(); | |
lensflare.addElement(new LensflareElement(textureFlare0, 700, 0)); | |
lensflare.addElement(new LensflareElement(textureFlare1, 512, 0.6)); | |
lensflare.addElement(new LensflareElement(textureFlare2, 170, 0.7)); | |
lensflare.addElement(new LensflareElement(textureFlare2, 120, 0.9)); | |
sunLight.add(lensflare); | |
const fillLight = new THREE.DirectionalLight(0xffffff, 1.2); | |
fillLight.position.set(-50, 50, -50); | |
scene.add(fillLight); | |
const rimLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
rimLight.position.set(0, 20, -100); | |
scene.add(rimLight); | |
// Controls setup | |
const controls = new PointerLockControls(camera, document.body); | |
document.addEventListener('click', function() { | |
controls.lock(); | |
}); | |
// Movement | |
let moveForward = false; | |
let moveBackward = false; | |
let moveLeft = false; | |
let moveRight = false; | |
let canJump = false; | |
const onKeyDown = function(event) { | |
switch(event.code) { | |
case 'KeyW': | |
case 'ArrowUp': | |
moveForward = true; | |
break; | |
case 'KeyA': | |
case 'ArrowLeft': | |
moveLeft = true; | |
break; | |
case 'KeyS': | |
case 'ArrowDown': | |
moveBackward = true; | |
break; | |
case 'KeyD': | |
case 'ArrowRight': | |
moveRight = true; | |
break; | |
case 'Space': | |
if (canJump) { | |
verticalVelocity = jumpForce; | |
canJump = false; | |
} | |
break; | |
} | |
}; | |
const onKeyUp = function(event) { | |
switch(event.code) { | |
case 'KeyW': | |
case 'ArrowUp': | |
moveForward = false; | |
break; | |
case 'KeyA': | |
case 'ArrowLeft': | |
moveLeft = false; | |
break; | |
case 'KeyS': | |
case 'ArrowDown': | |
moveBackward = false; | |
break; | |
case 'KeyD': | |
case 'ArrowRight': | |
moveRight = false; | |
break; | |
} | |
}; | |
document.addEventListener('keydown', onKeyDown); | |
document.addEventListener('keyup', onKeyUp); | |
// Physics variables | |
const gravity = -35; | |
let verticalVelocity = 0; | |
const playerHeight = 2; | |
const jumpForce = 14; | |
const groundErrorMargin = 0.2; // Added error margin for ground detection | |
let isGrounded = false; | |
const bounceCoefficient = 0.4; | |
// Raycaster for ground detection | |
const raycaster = new THREE.Raycaster(); | |
// Modified terrain loading with texture extraction | |
const loader = new GLTFLoader(); | |
let terrain; | |
// Gun model setup | |
let gunModel; | |
const gunLoader = new GLTFLoader(); | |
gunLoader.load('/public/models/laser-gun.glb', (gltf) => { | |
// Scale and position the gun relative to camera | |
gunModel = gltf.scene; | |
gunModel.scale.set(0.3, 0.3, 0.3); | |
gunModel.position.set(0.35, -0.23, -0.54); | |
gunModel.rotation.y = Math.PI * 1.6; | |
gunModel.rotation.z = Math.PI * 0.2; | |
gunModel.rotation.x = Math.PI * 0.1 + currentWeaponSway.x; | |
gunModel.rotation.y = Math.PI * 1.6 + currentWeaponSway.y; | |
gunModel.rotation.z = Math.PI * 0.2 + currentWeaponSway.z; | |
// Add the gun to the camera | |
camera.add(gunModel); | |
scene.add(camera); // Need to add camera to scene for gun to be visible | |
}); | |
loader.load('/public/models/pond-sparse.glb', (gltf) => { | |
console.log("model loaded! preparing it.."); | |
terrain = gltf.scene; | |
terrain.scale.set(100, 100, 100); | |
terrain.rotation.y = Math.PI; | |
if (USE_SPARSE) { | |
terrain.rotation.x = -Math.PI * 2.15; | |
} | |
let textureFound = false; | |
terrain.traverse((node) => { | |
if (node.isMesh) { | |
node.castShadow = true; | |
node.receiveShadow = true; | |
if (node.material) { | |
node.material.roughness = 0.8; | |
node.material.metalness = 0.2; | |
// Extract texture if available | |
if (node.material.map && !textureFound) { | |
extractedTexture = extractAndSaveTexture(node); | |
if (extractedTexture) { | |
downloadButton.disabled = false; | |
textureFound = true; | |
} | |
} | |
} | |
} | |
}); | |
scene.add(terrain); | |
const box = new THREE.Box3().setFromObject(terrain); | |
const center = box.getCenter(new THREE.Vector3()); | |
terrain.position.x -= center.x; | |
terrain.position.z -= center.z; | |
terrain.position.y = 0; | |
raycaster.ray.origin.set(0, 100, 10); | |
raycaster.ray.direction.set(0, -1, 0); | |
const intersects = raycaster.intersectObjects(scene.children, true); | |
if (intersects.length > 0) { | |
camera.position.set(0, intersects[0].point.y + 2.5, 10); | |
} else { | |
camera.position.set(0, 20, 10); | |
} | |
camera.lookAt(new THREE.Vector3(0, 0, 0)); | |
}, | |
undefined, | |
(error) => { | |
console.error('An error happened:', error); | |
const geometry = new THREE.PlaneGeometry(100, 100, 20, 20); | |
const material = new THREE.MeshStandardMaterial({ | |
color: 0x808080, | |
roughness: 0.8, | |
metalness: 0.2, | |
wireframe: true | |
}); | |
terrain = new THREE.Mesh(geometry, material); | |
terrain.castShadow = true; | |
terrain.receiveShadow = true; | |
scene.add(terrain); | |
}); | |
// Movement speed and physics | |
const velocity = new THREE.Vector3(); | |
const direction = new THREE.Vector3(); | |
const speed = 0.5; | |
const delta = 1/60; | |
function checkGround() { | |
if (!terrain) return { | |
distance: Infinity, | |
normal: new THREE.Vector3(0, 1, 0), | |
hasHeadCollision: false | |
}; | |
// Ground check ray | |
raycaster.ray.origin.copy(camera.position); | |
raycaster.ray.direction.set(0, -1, 0); | |
const groundIntersects = raycaster.intersectObjects(scene.children, true); | |
// Head collision check ray | |
raycaster.ray.origin.copy(camera.position); | |
raycaster.ray.direction.set(0, 1, 0); | |
const headIntersects = raycaster.intersectObjects(scene.children, true); | |
const hasHeadCollision = headIntersects.length > 0 && headIntersects[0].distance < 1.0; | |
if (groundIntersects.length > 0) { | |
return { | |
distance: groundIntersects[0].distance, | |
normal: groundIntersects[0].face.normal.clone(), | |
hasHeadCollision: hasHeadCollision | |
}; | |
} | |
return { | |
distance: Infinity, | |
normal: new THREE.Vector3(0, 1, 0), | |
hasHeadCollision: hasHeadCollision | |
}; | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
if(controls.isLocked) { | |
const groundInfo = checkGround(); | |
const distanceToGround = groundInfo.distance; | |
// Calculate current movement speed | |
const currentSpeed = Math.sqrt(velocity.x * velocity.x + velocity.z * velocity.z); | |
// Update weapon sway based on movement | |
updateWeaponSway(currentSpeed); | |
updateGunPosition(); | |
// Check if we're below terrain or if there's no terrain below us | |
raycaster.ray.origin.copy(camera.position); | |
raycaster.ray.direction.set(0, -1, 0); | |
const belowIntersects = raycaster.intersectObjects(scene.children, true); | |
// Check if we're below terrain by casting a ray upward | |
raycaster.ray.origin.copy(camera.position); | |
raycaster.ray.direction.set(0, 1, 0); | |
const aboveIntersects = raycaster.intersectObjects(scene.children, true); | |
// If we're below terrain (ray hits something above us) or if there's no ground below us | |
if ((aboveIntersects.length > 0 && aboveIntersects[0].distance < playerHeight) || | |
belowIntersects.length === 0) { | |
// Find a safe position above terrain | |
raycaster.ray.origin.set(camera.position.x, 200, camera.position.z); | |
raycaster.ray.direction.set(0, -1, 0); | |
const rescueIntersects = raycaster.intersectObjects(scene.children, true); | |
if (rescueIntersects.length > 0) { | |
// Teleport player to safety | |
camera.position.y = rescueIntersects[0].point.y + playerHeight; | |
verticalVelocity = 0; | |
isGrounded = true; | |
canJump = true; | |
} else { | |
// If no safe position found, reset to initial position | |
camera.position.set(0, 20, 10); | |
verticalVelocity = 0; | |
} | |
} | |
// Handle head collisions | |
if (groundInfo.hasHeadCollision && verticalVelocity > 0) { | |
verticalVelocity = 0; | |
} | |
const slopeAngle = Math.acos(groundInfo.normal.dot(new THREE.Vector3(0, 1, 0))); | |
const maxClimbableAngle = Math.PI / 4; | |
// Improved ground detection with error margin | |
if (distanceToGround > playerHeight + groundErrorMargin) { | |
verticalVelocity += gravity * delta; | |
isGrounded = false; | |
} else if (distanceToGround < playerHeight - groundErrorMargin) { | |
if (verticalVelocity < 0) { | |
verticalVelocity = Math.abs(verticalVelocity) * bounceCoefficient; | |
camera.position.y = camera.position.y + (playerHeight - distanceToGround); | |
} | |
isGrounded = true; | |
canJump = true; | |
} else { | |
if (Math.abs(verticalVelocity) < 0.1) { | |
verticalVelocity = 0; | |
isGrounded = true; | |
canJump = true; | |
} else { | |
verticalVelocity *= 0.8; | |
isGrounded = false; | |
} | |
} | |
verticalVelocity = Math.max(verticalVelocity, -20); | |
camera.position.y += verticalVelocity * delta; | |
direction.z = Number(moveForward) - Number(moveBackward); | |
direction.x = Number(moveRight) - Number(moveLeft); | |
direction.normalize(); | |
if(moveForward || moveBackward) velocity.z = -direction.z * speed; | |
if(moveLeft || moveRight) velocity.x = -direction.x * speed; | |
const moveDirection = new THREE.Vector3(-velocity.x, 0, -velocity.z).normalize(); | |
const slopeDirection = groundInfo.normal.clone().projectOnPlane(new THREE.Vector3(0, 1, 0)).normalize(); | |
const slopeFactor = 1.0 - (moveDirection.dot(slopeDirection) * (slopeAngle / (Math.PI / 2))); | |
const slopeSpeedMultiplier = slopeFactor > 0 | |
? 1.0 - (Math.min(slopeFactor, 1.0) * 0.5) | |
: 1.0 + (Math.min(Math.abs(slopeFactor), 1.0) * 0.3); | |
const movementSpeed = isGrounded ? speed * slopeSpeedMultiplier : speed * 0.8; | |
if (moveForward || moveBackward || moveLeft || moveRight) { | |
controls.moveRight(-velocity.x * movementSpeed); | |
controls.moveForward(-velocity.z * movementSpeed); | |
} else { | |
controls.moveRight(-velocity.x * movementSpeed); | |
controls.moveForward(-velocity.z * movementSpeed); | |
} | |
velocity.x *= 0.9; | |
velocity.z *= 0.9; | |
} | |
// Update sky and render | |
const time = performance.now() * 0.0001; | |
const distance = 400000; | |
sun.x = distance * Math.cos(time); | |
sun.y = distance * Math.sin(time) * 1.25; | |
sun.z = distance * Math.sin(time) * 0.25; | |
sky.material.uniforms['sunPosition'].value.copy(sun); | |
sunLight.position.copy(sun).normalize().multiplyScalar(500); | |
renderer.render(scene, camera); | |
} | |
function updateWeaponSway(currentSpeed) { | |
const now = performance.now(); | |
if (now - lastSwayUpdate < swayUpdateInterval) return; | |
lastSwayUpdate = now; | |
// Calculate sway amount based on movement speed | |
const movementFactor = Math.min(currentSpeed / speed, 1); | |
const swayAmount = maxSwayAmount * movementFactor; | |
// Generate random sway targets | |
targetWeaponSway.x = (Math.random() * 2 - 1) * swayAmount; | |
targetWeaponSway.y = (Math.random() * 2 - 1) * swayAmount; | |
targetWeaponSway.z = (Math.random() * 2 - 1) * swayAmount * 0.5; | |
} | |
function updateGunPosition() { | |
if (!gunModel) return; | |
// Interpolate current sway towards target | |
currentWeaponSway.x += (targetWeaponSway.x - currentWeaponSway.x) * swayLerpFactor; | |
currentWeaponSway.y += (targetWeaponSway.y - currentWeaponSway.y) * swayLerpFactor; | |
currentWeaponSway.z += (targetWeaponSway.z - currentWeaponSway.z) * swayLerpFactor; | |
// Apply sway to gun model's rotation | |
gunModel.rotation.x = Math.PI * 0.1 + currentWeaponSway.x; | |
gunModel.rotation.y = Math.PI * 1.6 + currentWeaponSway.y; | |
gunModel.rotation.z = Math.PI * 0.2 + currentWeaponSway.z; | |
} | |
// Handle window resize | |
window.addEventListener('resize', onWindowResize, false); | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
animate(); | |
</script> | |
</body> | |
</html> | |