3DCityScene / index.html
awacke1's picture
Update index.html
c5d5414 verified
raw
history blame
34 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D City Scene</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
}
canvas {
display: block;
}
.ui-container {
position: absolute;
top: 10px;
left: 10px;
color: white;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
user-select: none;
}
.controls {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="ui-container">
<h2>3D City Explorer</h2>
<div id="score">Score: 0</div>
<div id="time">Time: 60</div>
</div>
<div class="controls">
<p>Controls: W to move backward, S to move forward, A/D to move left/right, Mouse to look, Space to jump</p>
<p>Collect the floating cubes to score points!</p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Polyfill for tqdm since it's not defined
const tqdm = {
tqdm: function(desc, total) {
return {
update: function() {},
// Other methods as needed
};
}
};
</script>
<script>
// Game variables
let score = 0;
let timeRemaining = 60;
let gameActive = true;
// Set up scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB); // Sky blue background
// Set up camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 15);
// Set up renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Add lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 50, 50);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 1;
directionalLight.shadow.camera.far = 500;
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -100;
scene.add(directionalLight);
// Create ground
const groundGeometry = new THREE.PlaneGeometry(200, 200);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x1a5e1a, // Green
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);
// Buildings array
const buildings = [];
const collectibles = [];
// Building color palette
const buildingColors = [
0x888888, 0x666666, 0x999999, 0xaaaaaa, 0x555555,
0x334455, 0x445566, 0x223344, 0x556677, 0x667788,
0x993333, 0x884422, 0x553333, 0x772222, 0x664433
];
// L-system grammar rules for buildings
const buildingRules = [
// Colonial style - symmetrical with central features
{
name: "Colonial",
axiom: "A",
rules: {
"A": "B[+F][-F]",
"B": "F[-C][+C]F",
"C": "D[-E][+E]",
"D": "F[+F][-F]F",
"E": "F[-F][+F]"
},
iterations: 2,
baseHeight: 10,
baseWidth: 6,
baseDepth: 6,
angle: Math.PI/6,
probability: 0.2
},
// Victorian style - complex with many decorative elements
{
name: "Victorian",
axiom: "A",
rules: {
"A": "B[+C][-C][/D][\\D]",
"B": "F[+F][-F][/F][\\F]",
"C": "F[++F][--F]",
"D": "F[+\F][-/F]"
},
iterations: 3,
baseHeight: 15,
baseWidth: 5,
baseDepth: 5,
angle: Math.PI/5,
probability: 0.15
},
// Modern style - clean lines, boxy but with variations
{
name: "Modern",
axiom: "A",
rules: {
"A": "B[+B][-B]",
"B": "F[/C][\\C]",
"C": "F"
},
iterations: 2,
baseHeight: 20,
baseWidth: 8,
baseDepth: 8,
angle: Math.PI/2,
probability: 0.25
},
// Skyscraper - tall vertical structures
{
name: "Skyscraper",
axiom: "A",
rules: {
"A": "FB[+C][-C]",
"B": "FB",
"C": "F[+F][-F]"
},
iterations: 4,
baseHeight: 30,
baseWidth: 10,
baseDepth: 10,
angle: Math.PI/8,
probability: 0.15
},
// Simple box building - for variety and filling space
{
name: "Simple",
axiom: "F",
rules: {
"F": "F[+F][-F]"
},
iterations: 1,
baseHeight: 8,
baseWidth: 6,
baseDepth: 6,
angle: Math.PI/4,
probability: 0.25
}
];
// L-system interpreter
function interpretLSystem(rule, position, rotation) {
// Start with the axiom
let currentString = rule.axiom;
// Apply rules for the specified number of iterations
for (let i = 0; i < rule.iterations; i++) {
let newString = "";
// Apply rules to each character
for (let j = 0; j < currentString.length; j++) {
const char = currentString[j];
newString += rule.rules[char] || char;
}
currentString = newString;
}
// Now interpret the L-system string to create building parts
let buildingGroup = new THREE.Group();
buildingGroup.position.copy(position);
// Stack to keep track of transformations
const stack = [];
let currentPosition = new THREE.Vector3(0, 0, 0);
let currentRotation = rotation || new THREE.Euler();
let scale = new THREE.Vector3(1, 1, 1);
// Select a material for this building
const color = buildingColors[Math.floor(Math.random() * buildingColors.length)];
const material = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.7,
metalness: 0.2
});
// Interpret each character in the final string
for (let i = 0; i < currentString.length; i++) {
const char = currentString[i];
switch (char) {
case 'F': // Forward and create a building part
// Randomize dimensions with constraints based on rule
const width = rule.baseWidth * (0.5 + Math.random() * 0.5) * scale.x;
const height = rule.baseHeight * (0.5 + Math.random() * 0.5) * scale.y;
const depth = rule.baseDepth * (0.5 + Math.random() * 0.5) * scale.z;
// Create geometry
const geometry = new THREE.BoxGeometry(width, height, depth);
const buildingPart = new THREE.Mesh(geometry, material);
// Position and add to group
buildingPart.position.copy(currentPosition);
buildingPart.rotation.copy(currentRotation);
buildingPart.castShadow = true;
buildingPart.receiveShadow = true;
// Add windows if part is large enough
if (height > 5 && width > 2 && depth > 2) {
addWindowsToBuilding(buildingPart, width, height, depth);
}
buildingGroup.add(buildingPart);
// Move forward in the direction of current rotation
const direction = new THREE.Vector3(0, height/2, 0);
direction.applyEuler(currentRotation);
currentPosition.add(direction);
break;
case '+': // Rotate right around Y axis
currentRotation.y += rule.angle;
break;
case '-': // Rotate left around Y axis
currentRotation.y -= rule.angle;
break;
case '/': // Rotate around X axis
currentRotation.x += rule.angle;
break;
case '\\': // Rotate around X axis (opposite)
currentRotation.x -= rule.angle;
break;
case '^': // Rotate around Z axis
currentRotation.z += rule.angle;
break;
case '&': // Rotate around Z axis (opposite)
currentRotation.z -= rule.angle;
break;
case '[': // Push state
stack.push({
position: currentPosition.clone(),
rotation: currentRotation.clone(),
scale: scale.clone()
});
break;
case ']': // Pop state
if (stack.length > 0) {
const state = stack.pop();
currentPosition = state.position;
currentRotation = state.rotation;
scale = state.scale;
}
break;
case '>': // Scale up
scale.multiplyScalar(1.2);
break;
case '<': // Scale down
scale.multiplyScalar(0.8);
break;
}
}
return buildingGroup;
}
// Create a city
function createCity() {
// Create grid of buildings
const citySize = 5; // Size of the city grid
const spacing = 15; // Spacing between building centers
for (let x = -citySize; x <= citySize; x++) {
for (let z = -citySize; z <= citySize; z++) {
// Skip sometimes to create spaces
if (Math.random() < 0.2) continue;
// Position with slight randomization
const position = new THREE.Vector3(
x * spacing + (Math.random() * 2 - 1), // Add slight randomness
0, // Will be adjusted by the L-system
z * spacing + (Math.random() * 2 - 1)
);
// Select a building style based on probability
let selectedRule = null;
let random = Math.random();
let cumulativeProbability = 0;
for (const rule of buildingRules) {
cumulativeProbability += rule.probability;
if (random <= cumulativeProbability) {
selectedRule = rule;
break;
}
}
if (!selectedRule) {
selectedRule = buildingRules[0]; // Default to first rule if somehow none selected
}
// Create building using L-system
const building = interpretLSystem(selectedRule, position, new THREE.Euler());
scene.add(building);
buildings.push(building);
}
}
// Create streets
const roadWidth = 8;
const roadColor = 0x333333;
// X-axis roads
for (let x = -citySize; x <= citySize; x++) {
const roadGeometry = new THREE.PlaneGeometry(roadWidth, citySize * 2 * spacing + roadWidth);
const roadMaterial = new THREE.MeshStandardMaterial({
color: roadColor,
roughness: 0.9,
metalness: 0.1
});
const road = new THREE.Mesh(roadGeometry, roadMaterial);
road.rotation.x = -Math.PI / 2;
road.position.set(x * spacing, 0.01, 0); // Slightly above ground to prevent z-fighting
scene.add(road);
// Add road markings
const markingGeometry = new THREE.PlaneGeometry(0.5, citySize * 2 * spacing + roadWidth);
const markingMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff });
const marking = new THREE.Mesh(markingGeometry, markingMaterial);
marking.rotation.x = -Math.PI / 2;
marking.position.set(x * spacing, 0.02, 0);
scene.add(marking);
}
// Z-axis roads
for (let z = -citySize; z <= citySize; z++) {
const roadGeometry = new THREE.PlaneGeometry(citySize * 2 * spacing + roadWidth, roadWidth);
const roadMaterial = new THREE.MeshStandardMaterial({
color: roadColor,
roughness: 0.9,
metalness: 0.1
});
const road = new THREE.Mesh(roadGeometry, roadMaterial);
road.rotation.x = -Math.PI / 2;
road.position.set(0, 0.01, z * spacing);
scene.add(road);
// Add road markings
const markingGeometry = new THREE.PlaneGeometry(citySize * 2 * spacing + roadWidth, 0.5);
const markingMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff });
const marking = new THREE.Mesh(markingGeometry, markingMaterial);
marking.rotation.x = -Math.PI / 2;
marking.position.set(0, 0.02, z * spacing);
scene.add(marking);
}
}
// Add windows to a building
function addWindowsToBuilding(building, width, height, depth) {
const windowSize = 0.5;
const windowSpacing = 1.5;
const windowGeometry = new THREE.PlaneGeometry(windowSize, windowSize);
const windowMaterial = new THREE.MeshStandardMaterial({
color: 0xffffcc,
emissive: 0xffffcc,
emissiveIntensity: 0.5,
transparent: true,
opacity: 0.8
});
// Calculate how many levels of windows to add based on building height
const numLevels = Math.floor((height - 2) / windowSpacing);
// Front and back windows
const frontZ = depth / 2 + 0.01;
const backZ = -depth / 2 - 0.01;
for (let level = 0; level < numLevels; level++) {
// Calculate y position for this level, starting from near the bottom
const y = 1 + level * windowSpacing;
for (let x = -width / 2 + windowSpacing; x < width / 2 - windowSpacing / 2; x += windowSpacing) {
// Only add some windows randomly
if (Math.random() < 0.3) continue;
// Front window
const frontWindow = new THREE.Mesh(windowGeometry, windowMaterial);
frontWindow.position.set(x, y, frontZ);
frontWindow.rotation.y = Math.PI;
building.add(frontWindow);
// Back window
const backWindow = new THREE.Mesh(windowGeometry, windowMaterial);
backWindow.position.set(x, y, backZ);
building.add(backWindow);
}
}
// Side windows
const rightX = width / 2 + 0.01;
const leftX = -width / 2 - 0.01;
for (let level = 0; level < numLevels; level++) {
// Calculate y position for this level, starting from near the bottom
const y = 1 + level * windowSpacing;
for (let z = -depth / 2 + windowSpacing; z < depth / 2 - windowSpacing / 2; z += windowSpacing) {
// Only add some windows randomly
if (Math.random() < 0.3) continue;
// Right window
const rightWindow = new THREE.Mesh(windowGeometry, windowMaterial);
rightWindow.position.set(rightX, y, z);
rightWindow.rotation.y = Math.PI / 2;
building.add(rightWindow);
// Left window
const leftWindow = new THREE.Mesh(windowGeometry, windowMaterial);
leftWindow.position.set(leftX, y, z);
leftWindow.rotation.y = -Math.PI / 2;
building.add(leftWindow);
}
}
}
// Create collectible items
function createCollectibles() {
const citySize = 5;
const spacing = 15;
for (let i = 0; i < 20; i++) {
const x = (Math.random() * 2 - 1) * citySize * spacing;
const z = (Math.random() * 2 - 1) * citySize * spacing;
const y = 1 + Math.random() * 20;
const collectibleGeometry = new THREE.BoxGeometry(1, 1, 1);
const collectibleMaterial = new THREE.MeshStandardMaterial({
color: 0xffff00,
emissive: 0xffff00,
emissiveIntensity: 0.5,
transparent: true,
opacity: 0.8
});
const collectible = new THREE.Mesh(collectibleGeometry, collectibleMaterial);
collectible.position.set(x, y, z);
collectible.userData.id = i;
collectible.userData.rotationSpeed = 0.01 + Math.random() * 0.02;
collectible.userData.floatSpeed = 0.5 + Math.random() * 0.5;
collectible.userData.floatRange = 0.5 + Math.random() * 0.5;
collectible.userData.initialY = y;
scene.add(collectible);
collectibles.push(collectible);
}
}
// Create skybox with clouds
function createSkybox() {
const skyGeometry = new THREE.SphereGeometry(400, 32, 32);
const skyMaterial = new THREE.MeshBasicMaterial({
color: 0x87CEEB,
side: THREE.BackSide
});
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
scene.add(sky);
// Add clouds
for (let i = 0; i < 50; i++) {
const radius = 350;
const phi = Math.random() * Math.PI;
const theta = Math.random() * Math.PI * 2;
const x = radius * Math.sin(phi) * Math.cos(theta);
const y = radius * Math.cos(phi) + 50; // Keep clouds higher in the sky
const z = radius * Math.sin(phi) * Math.sin(theta);
const cloudSize = 10 + Math.random() * 20;
const cloudGeometry = new THREE.SphereGeometry(cloudSize, 8, 8);
const cloudMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 1,
metalness: 0,
transparent: true,
opacity: 0.8
});
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
cloud.position.set(x, y, z);
cloud.userData.rotationSpeed = 0.0001 + Math.random() * 0.0002;
scene.add(cloud);
}
}
// Player object
const playerHeight = 2;
const playerRadius = 0.5;
// Use cylinder instead of capsule for compatibility with r128
const playerGeometry = new THREE.CylinderGeometry(playerRadius, playerRadius, playerHeight, 8);
const playerMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff });
const player = new THREE.Mesh(playerGeometry, playerMaterial);
player.position.set(0, playerHeight / 2, 0);
player.castShadow = true;
scene.add(player);
// Player physics
const playerVelocity = new THREE.Vector3();
const playerDirection = new THREE.Vector3();
let isJumping = false;
const GRAVITY = 0.2;
const JUMP_FORCE = 0.7;
const MOVE_SPEED = 0.2;
// Player collision detection
function checkPlayerCollisions() {
// Collectible collisions
for (let i = collectibles.length - 1; i >= 0; i--) {
const collectible = collectibles[i];
const distance = player.position.distanceTo(collectible.position);
if (distance < playerRadius + 1) {
scene.remove(collectible);
collectibles.splice(i, 1);
score += 10;
document.getElementById('score').textContent = `Score: ${score}`;
}
}
// Building collisions - updated for composite buildings
for (const building of buildings) {
// For each building group, we need to check collision with each child
building.traverse((child) => {
if (child.isMesh) {
const buildingBox = new THREE.Box3().setFromObject(child);
const playerPos = player.position.clone();
// Check if player is inside building bounds but add some margin for the radius
if (playerPos.x + playerRadius > buildingBox.min.x &&
playerPos.x - playerRadius < buildingBox.max.x &&
playerPos.z + playerRadius > buildingBox.min.z &&
playerPos.z - playerRadius < buildingBox.max.z) {
// Find the closest edge to push the player away from
const edgeDistances = [
{edge: 'left', dist: Math.abs(playerPos.x - buildingBox.min.x)},
{edge: 'right', dist: Math.abs(playerPos.x - buildingBox.max.x)},
{edge: 'front', dist: Math.abs(playerPos.z - buildingBox.min.z)},
{edge: 'back', dist: Math.abs(playerPos.z - buildingBox.max.z)}
];
edgeDistances.sort((a, b) => a.dist - b.dist);
const closestEdge = edgeDistances[0].edge;
// Push player away from the closest edge
switch (closestEdge) {
case 'left':
player.position.x = buildingBox.min.x - playerRadius;
break;
case 'right':
player.position.x = buildingBox.max.x + playerRadius;
break;
case 'front':
player.position.z = buildingBox.min.z - playerRadius;
break;
case 'back':
player.position.z = buildingBox.max.z + playerRadius;
break;
}
}
}
});
}
// Floor collision and jumping physics
if (player.position.y <= playerHeight / 2) {
player.position.y = playerHeight / 2;
playerVelocity.y = 0;
isJumping = false;
}
}
// Controls
const keys = {
w: false,
a: false,
s: false,
d: false,
space: false
};
// Mouse controls for looking around
const mousePosition = {
x: 0,
y: 0
};
let cameraRotation = 0;
let cameraPitch = 0;
document.addEventListener('keydown', (event) => {
switch (event.key.toLowerCase()) {
case 'w': keys.w = true; break;
case 'a': keys.a = true; break;
case 's': keys.s = true; break;
case 'd': keys.d = true; break;
case ' ': keys.space = true; break;
}
});
document.addEventListener('keyup', (event) => {
switch (event.key.toLowerCase()) {
case 'w': keys.w = false; break;
case 'a': keys.a = false; break;
case 's': keys.s = false; break;
case 'd': keys.d = false; break;
case ' ': keys.space = false; break;
}
});
document.addEventListener('mousemove', (event) => {
// Only capture mouse if pointer is locked
if (document.pointerLockElement === renderer.domElement) {
cameraRotation -= event.movementX * 0.002;
cameraPitch -= event.movementY * 0.002;
// Limit pitch to prevent camera flipping
cameraPitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, cameraPitch));
}
});
// Lock pointer when clicking on the game
renderer.domElement.addEventListener('click', () => {
if (!document.pointerLockElement) {
renderer.domElement.requestPointerLock();
}
});
// Update player position based on input
function updatePlayer() {
// Apply gravity
playerVelocity.y -= GRAVITY;
// Handle jumping
if (keys.space && !isJumping) {
playerVelocity.y = JUMP_FORCE;
isJumping = true;
}
// Get movement direction based on camera rotation
// Switching W and S keys (W now moves backward, S moves forward)
playerDirection.z = Number(keys.s) - Number(keys.w);
playerDirection.x = Number(keys.d) - Number(keys.a);
playerDirection.normalize();
// Rotate direction based on camera rotation
playerDirection.applyAxisAngle(new THREE.Vector3(0, 1, 0), cameraRotation);
// Apply movement
player.position.x += playerDirection.x * MOVE_SPEED;
player.position.z += playerDirection.z * MOVE_SPEED;
player.position.y += playerVelocity.y;
// Update camera position to follow player
camera.position.x = player.position.x;
camera.position.z = player.position.z;
camera.position.y = player.position.y + 1.5; // Eye level
// Update camera rotation
camera.rotation.order = 'YXZ'; // Important for proper rotation
camera.rotation.y = cameraRotation;
camera.rotation.x = cameraPitch;
// Collision detection
checkPlayerCollisions();
}
// Timer function
function updateTimer() {
if (gameActive && timeRemaining > 0) {
timeRemaining--;
document.getElementById('time').textContent = `Time: ${timeRemaining}`;
if (timeRemaining === 0) {
gameActive = false;
endGame();
}
}
}
// End game
function endGame() {
const endScreen = document.createElement('div');
endScreen.style.position = 'absolute';
endScreen.style.top = '50%';
endScreen.style.left = '50%';
endScreen.style.transform = 'translate(-50%, -50%)';
endScreen.style.background = 'rgba(0, 0, 0, 0.8)';
endScreen.style.color = 'white';
endScreen.style.padding = '20px';
endScreen.style.borderRadius = '10px';
endScreen.style.textAlign = 'center';
endScreen.innerHTML = `
<h2>Game Over!</h2>
<p>Your final score: ${score}</p>
<button id="restart-btn" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px;">Play Again</button>
`;
document.body.appendChild(endScreen);
document.getElementById('restart-btn').addEventListener('click', () => {
document.body.removeChild(endScreen);
score = 0;
timeRemaining = 60;
gameActive = true;
document.getElementById('score').textContent = `Score: ${score}`;
document.getElementById('time').textContent = `Time: ${timeRemaining}`;
// Reset player position
player.position.set(0, playerHeight / 2, 0);
playerVelocity.set(0, 0, 0);
// Reset camera
cameraRotation = 0;
cameraPitch = 0;
// Reset collectibles
for (const collectible of collectibles) {
scene.remove(collectible);
}
collectibles.length = 0;
createCollectibles();
});
}
// Animation and game loop
function animate() {
requestAnimationFrame(animate);
if (gameActive) {
updatePlayer();
// Animate collectibles
for (const collectible of collectibles) {
collectible.rotation.x += collectible.userData.rotationSpeed;
collectible.rotation.y += collectible.userData.rotationSpeed * 1.5;
// Float up and down
const floatOffset = Math.sin(Date.now() * 0.001 * collectible.userData.floatSpeed) * collectible.userData.floatRange;
collectible.position.y = collectible.userData.initialY + floatOffset;
}
}
renderer.render(scene, camera);
}
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Initialize
createCity();
createCollectibles();
createSkybox();
// Start game timer
setInterval(updateTimer, 1000);
// Start animation loop
animate();
</script>
</body>
</html>