the-pond / index.html
jbilcke-hf's picture
jbilcke-hf HF staff
Upload 9 files
45931a3 verified
<!DOCTYPE html>
<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>