Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Three.js Dynamic Simulated City - Corner Columns</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<style> | |
body { margin: 0; overflow: hidden; background-color: #000000; color: #e2e8f0; font-family: 'Inter', sans-serif; } | |
canvas { display: block; } | |
#infoPanel { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
background-color: rgba(0,0,0,0.75); | |
padding: 15px; | |
border-radius: 8px; | |
color: white; | |
font-size: 0.85em; | |
max-width: 320px; | |
box-shadow: 0 4px 12px rgba(0,0,0,0.5); | |
max-height: 90vh; | |
overflow-y: auto; | |
} | |
#infoPanel h2 { | |
margin-top: 0; | |
font-size: 1.1em; | |
border-bottom: 1px solid #4a5568; | |
padding-bottom: 5px; | |
margin-bottom: 10px; | |
} | |
#infoPanel p, #infoPanel ul { | |
margin-bottom: 8px; | |
line-height: 1.5; | |
} | |
#infoPanel ul { | |
list-style: disc; | |
padding-left: 20px; | |
} | |
#loadingScreen { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: #111827; /* Darker initial loading */ | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 9999; | |
color: white; | |
font-size: 1.5em; | |
} | |
.spinner { | |
border: 4px solid rgba(255, 255, 255, 0.3); | |
border-radius: 50%; | |
border-top: 4px solid #fff; | |
width: 40px; | |
height: 40px; | |
animation: spin 1s linear infinite; | |
margin-bottom: 20px; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
</style> | |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"> | |
</head> | |
<body> | |
<div id="loadingScreen"> | |
<div class="spinner"></div> | |
Loading Dynamic City... | |
</div> | |
<div id="infoPanel"> | |
<h2>Dynamic Simulated City - V4</h2> | |
<p>Added stone-colored cylinder columns to building corners.</p> | |
<p><strong>Features Added/Improved:</strong></p> | |
<ul> | |
<li>Moving cars and people.</li> | |
<li>Birds flying.</li> | |
<li>Recessed building windows with lights.</li> | |
<li>Solid roof caps on buildings.</li> | |
<li>Internal floor slabs.</li> | |
<li>Corner columns on buildings.</li> | |
<li>Day/Night cycle with Sun & Moon.</li> | |
</ul> | |
<p><em>Use mouse to orbit, scroll to zoom, right-click to pan.</em></p> | |
</div> | |
<canvas id="cityCanvas"></canvas> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"three": "https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js", | |
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.164.1/examples/jsm/" | |
} | |
} | |
</script> | |
<script type="module"> | |
import * as THREE from 'three'; | |
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
let scene, camera, renderer, controls; | |
const buildings = []; | |
const vehicles = []; | |
const pedestrians = []; | |
const birds = []; | |
const citySize = 20; | |
const buildingSpacing = 2.0; | |
const roadWidth = 0.5; | |
const buildingMaxHeight = 8; | |
const buildingMinHeight = 1; | |
const roofHeight = 0.2; | |
const floorSlabThickness = 0.05; | |
const typicalFloorHeight = 0.5; | |
const columnRadius = 0.1; // Radius of the corner columns | |
let sunLight, moonLight, ambientLight; | |
const skyRadius = citySize * buildingSpacing * 1.5; | |
const dayClearColor = new THREE.Color(0x87CEEB); | |
const nightClearColor = new THREE.Color(0x000020); | |
const dayFogColor = new THREE.Color(0x87CEEB); | |
const nightFogColor = new THREE.Color(0x000010); | |
// --- Core Three.js Setup --- | |
function init() { | |
const canvas = document.getElementById('cityCanvas'); | |
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.setPixelRatio(window.devicePixelRatio); | |
renderer.shadowMap.enabled = true; | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
renderer.toneMappingExposure = 0.8; | |
scene = new THREE.Scene(); | |
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, skyRadius * 2.5); | |
camera.position.set(citySize * 0.7, citySize * 0.6, citySize * 0.7); | |
controls = new OrbitControls(camera, renderer.domElement); | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
controls.screenSpacePanning = false; | |
controls.minDistance = 3; | |
controls.maxDistance = citySize * 2.5; | |
controls.maxPolarAngle = Math.PI / 2 - 0.01; | |
// --- Lighting --- | |
ambientLight = new THREE.AmbientLight(0xffffff, 0.1); | |
scene.add(ambientLight); | |
sunLight = new THREE.DirectionalLight(0xffffee, 0); | |
sunLight.castShadow = true; | |
sunLight.shadow.mapSize.width = 2048; | |
sunLight.shadow.mapSize.height = 2048; | |
sunLight.shadow.camera.near = 0.5; | |
sunLight.shadow.camera.far = skyRadius * 0.8; | |
sunLight.shadow.camera.left = -citySize * 1.5; | |
sunLight.shadow.camera.right = citySize * 1.5; | |
sunLight.shadow.camera.top = citySize * 1.5; | |
sunLight.shadow.camera.bottom = -citySize * 1.5; | |
sunLight.shadow.bias = -0.0005; | |
scene.add(sunLight); | |
moonLight = new THREE.DirectionalLight(0x7788cc, 0); | |
moonLight.castShadow = true; | |
moonLight.shadow.mapSize.width = 1024; | |
moonLight.shadow.mapSize.height = 1024; | |
moonLight.shadow.camera.near = 0.5; | |
moonLight.shadow.camera.far = skyRadius * 0.8; | |
moonLight.shadow.bias = -0.0005; | |
scene.add(moonLight); | |
// --- Ground --- | |
const groundGeometry = new THREE.PlaneGeometry(citySize * buildingSpacing * 1.2, citySize * buildingSpacing * 1.2); | |
const groundMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x445544, | |
roughness: 0.9, | |
metalness: 0.1 | |
}); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
scene.add(ground); | |
// --- City, Entities --- | |
generateCity(); | |
createVehicles(30); | |
createPedestrians(50); | |
createBirds(20); | |
document.getElementById('loadingScreen').style.display = 'none'; | |
window.addEventListener('resize', onWindowResize, false); | |
animate(); | |
} | |
// --- Procedural City Generation with Windows, Roofs, Floor Slabs & Columns --- | |
function generateCity() { | |
const buildingBaseGeometry = new THREE.BoxGeometry(1, 1, 1); | |
const roofGeometry = new THREE.BoxGeometry(1, roofHeight, 1); | |
const floorSlabGeometry = new THREE.BoxGeometry(1, floorSlabThickness, 1); | |
const columnGeometry = new THREE.CylinderGeometry(columnRadius, columnRadius, 1, 12); // Height will be scaled | |
const floorSlabMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x606060, | |
roughness: 0.8, | |
metalness: 0.1, | |
}); | |
const columnMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x787878, // Stone grey | |
roughness: 0.7, | |
metalness: 0.05, | |
}); | |
for (let i = 0; i < citySize; i++) { | |
for (let j = 0; j < citySize; j++) { | |
if (Math.random() > 0.25) { | |
const buildingBodyHeight = THREE.MathUtils.randFloat(buildingMinHeight, buildingMaxHeight - roofHeight); | |
// Adjust building width/depth slightly to make space for columns visually, or columns will clip into facade | |
const buildingWidth = THREE.MathUtils.randFloat(0.7, buildingSpacing - roadWidth * 0.8 - columnRadius * 2.5); | |
const buildingDepth = THREE.MathUtils.randFloat(0.7, buildingSpacing - roadWidth * 0.8 - columnRadius * 2.5); | |
const buildingMaterial = new THREE.MeshStandardMaterial({ | |
color: new THREE.Color().setHSL(Math.random() * 0.1 + 0.55, 0.1, Math.random() * 0.2 + 0.3), | |
roughness: THREE.MathUtils.randFloat(0.6, 0.9), | |
metalness: THREE.MathUtils.randFloat(0.0, 0.2), | |
}); | |
// Create Building Body | |
const building = new THREE.Mesh(buildingBaseGeometry, buildingMaterial); | |
building.scale.set(buildingWidth, buildingBodyHeight, buildingDepth); | |
const buildingCenterX = (i - citySize / 2 + 0.5) * buildingSpacing; | |
const buildingCenterZ = (j - citySize / 2 + 0.5) * buildingSpacing; | |
building.position.set(buildingCenterX, buildingBodyHeight / 2, buildingCenterZ); | |
building.castShadow = true; | |
building.receiveShadow = true; | |
scene.add(building); | |
building.userData.windows = []; | |
// Create Roof Cap | |
const roofMaterial = buildingMaterial.clone(); | |
roofMaterial.color.offsetHSL(0, 0, -0.05); | |
const roof = new THREE.Mesh(roofGeometry, roofMaterial); | |
roof.scale.set(buildingWidth * 1.05 + columnRadius*2, 1, buildingDepth * 1.05 + columnRadius*2); // Roof overhangs columns | |
roof.position.set(buildingCenterX, buildingBodyHeight + roofHeight / 2, buildingCenterZ); | |
roof.castShadow = true; | |
roof.receiveShadow = true; | |
scene.add(roof); | |
// Add Corner Columns | |
const columnPositions = [ | |
{ x: buildingWidth / 2, z: buildingDepth / 2 }, | |
{ x: -buildingWidth / 2, z: buildingDepth / 2 }, | |
{ x: buildingWidth / 2, z: -buildingDepth / 2 }, | |
{ x: -buildingWidth / 2, z: -buildingDepth / 2 }, | |
]; | |
columnPositions.forEach(pos => { | |
const column = new THREE.Mesh(columnGeometry, columnMaterial.clone()); | |
column.scale.y = buildingBodyHeight; // Scale cylinder height | |
column.position.set( | |
buildingCenterX + pos.x, | |
buildingBodyHeight / 2, // Center of the column height | |
buildingCenterZ + pos.z | |
); | |
column.castShadow = true; | |
column.receiveShadow = true; | |
scene.add(column); | |
}); | |
// Add Floor Slabs | |
for (let fh = typicalFloorHeight; fh < buildingBodyHeight - typicalFloorHeight / 2; fh += typicalFloorHeight) { | |
const floorSlab = new THREE.Mesh(floorSlabGeometry, floorSlabMaterial.clone()); | |
floorSlab.scale.set(buildingWidth * 0.98, 1, buildingDepth * 0.98); // Slightly smaller than building body | |
const localY = fh - buildingBodyHeight / 2; | |
floorSlab.position.set(0, localY, 0); | |
floorSlab.castShadow = true; | |
floorSlab.receiveShadow = true; | |
building.add(floorSlab); | |
} | |
// Add Windows (to the building body) | |
const windowSize = 0.15; | |
const windowSpacingFactor = 0.25; | |
const windowRecessOffset = -0.02; | |
const maxWindowHeight = buildingBodyHeight - (windowSpacingFactor * buildingBodyHeight / 2) - (windowSize / 2); | |
// Facade Z+ (Front) | |
for (let bh = windowSpacingFactor * buildingBodyHeight / 2; bh < maxWindowHeight; bh += windowSpacingFactor * buildingBodyHeight) { | |
for (let bw = -buildingWidth / 2 + windowSpacingFactor * buildingWidth / 2; bw < buildingWidth / 2 - windowSpacingFactor * buildingWidth / 4; bw += windowSpacingFactor * buildingWidth * 0.75) { | |
if (Math.random() < 0.8) { | |
const windowMat = new THREE.MeshStandardMaterial({ | |
color: 0x050510, emissive: 0x000000, emissiveIntensity: 0, transparent: true, opacity: 0.65 | |
}); | |
const windowGeo = new THREE.PlaneGeometry(windowSize, windowSize); | |
const win = new THREE.Mesh(windowGeo, windowMat); | |
win.position.set(bw, bh - buildingBodyHeight / 2, buildingDepth / 2 + windowRecessOffset); | |
building.add(win); | |
building.userData.windows.push(win); | |
} | |
} | |
} | |
// Facade Z- (Back) | |
for (let bh = windowSpacingFactor * buildingBodyHeight / 2; bh < maxWindowHeight; bh += windowSpacingFactor * buildingBodyHeight) { | |
for (let bw = -buildingWidth / 2 + windowSpacingFactor * buildingWidth / 2; bw < buildingWidth / 2 - windowSpacingFactor * buildingWidth / 4; bw += windowSpacingFactor * buildingWidth * 0.75) { | |
if (Math.random() < 0.8) { | |
const windowMat = new THREE.MeshStandardMaterial({ | |
color: 0x050510, emissive: 0x000000, emissiveIntensity: 0, transparent: true, opacity: 0.65 | |
}); | |
const windowGeo = new THREE.PlaneGeometry(windowSize, windowSize); | |
const win = new THREE.Mesh(windowGeo, windowMat); | |
win.position.set(bw, bh - buildingBodyHeight / 2, -buildingDepth / 2 - windowRecessOffset); | |
win.rotation.y = Math.PI; | |
building.add(win); | |
building.userData.windows.push(win); | |
} | |
} | |
} | |
// Facade X+ (Right) | |
for (let bh = windowSpacingFactor * buildingBodyHeight / 2; bh < maxWindowHeight; bh += windowSpacingFactor * buildingBodyHeight) { | |
for (let bd = -buildingDepth / 2 + windowSpacingFactor * buildingDepth / 2; bd < buildingDepth / 2 - windowSpacingFactor * buildingDepth / 4; bd += windowSpacingFactor * buildingDepth * 0.75) { | |
if (Math.random() < 0.8) { | |
const windowMat = new THREE.MeshStandardMaterial({ | |
color: 0x050510, emissive: 0x000000, emissiveIntensity: 0, transparent: true, opacity: 0.65 | |
}); | |
const windowGeo = new THREE.PlaneGeometry(windowSize, windowSize); | |
const win = new THREE.Mesh(windowGeo, windowMat); | |
win.position.set(buildingWidth / 2 + windowRecessOffset, bh - buildingBodyHeight / 2, bd); | |
win.rotation.y = Math.PI / 2; | |
building.add(win); | |
building.userData.windows.push(win); | |
} | |
} | |
} | |
// Facade X- (Left) | |
for (let bh = windowSpacingFactor * buildingBodyHeight / 2; bh < maxWindowHeight; bh += windowSpacingFactor * buildingBodyHeight) { | |
for (let bd = -buildingDepth / 2 + windowSpacingFactor * buildingDepth / 2; bd < buildingDepth / 2 - windowSpacingFactor * buildingDepth / 4; bd += windowSpacingFactor * buildingDepth * 0.75) { | |
if (Math.random() < 0.8) { | |
const windowMat = new THREE.MeshStandardMaterial({ | |
color: 0x050510, emissive: 0x000000, emissiveIntensity: 0, transparent: true, opacity: 0.65 | |
}); | |
const windowGeo = new THREE.PlaneGeometry(windowSize, windowSize); | |
const win = new THREE.Mesh(windowGeo, windowMat); | |
win.position.set(-buildingWidth / 2 - windowRecessOffset, bh - buildingBodyHeight / 2, bd); | |
win.rotation.y = -Math.PI / 2; | |
building.add(win); | |
building.userData.windows.push(win); | |
} | |
} | |
} | |
buildings.push(building); | |
} | |
} | |
} | |
} | |
// --- Create Moving Entities (Unchanged) --- | |
function createVehicles(count) { | |
const carGeo = new THREE.BoxGeometry(0.6, 0.25, 0.3); | |
const carMat = new THREE.MeshStandardMaterial({ color: 0xaa0000, roughness: 0.3, metalness: 0.5 }); | |
for (let i = 0; i < count; i++) { | |
const vehicle = new THREE.Mesh(carGeo, carMat.clone()); | |
vehicle.material.color.setHSL(Math.random(), 0.7, 0.5); | |
vehicle.castShadow = true; | |
const onXAxis = Math.random() > 0.5; | |
const roadLine = Math.floor(Math.random() * citySize) - citySize / 2 + 0.5; | |
vehicle.position.set( | |
onXAxis ? (Math.random() - 0.5) * citySize * buildingSpacing : roadLine * buildingSpacing + (Math.random() > 0.5 ? roadWidth : -roadWidth), | |
0.125, | |
onXAxis ? roadLine * buildingSpacing + (Math.random() > 0.5 ? roadWidth : -roadWidth) : (Math.random() - 0.5) * citySize * buildingSpacing | |
); | |
vehicle.userData.speed = THREE.MathUtils.randFloat(0.02, 0.05) * (Math.random() > 0.5 ? 1 : -1); | |
vehicle.userData.axis = onXAxis ? 'x' : 'z'; | |
if (vehicle.userData.axis === 'x') { | |
vehicle.rotation.y = vehicle.userData.speed > 0 ? 0 : Math.PI; | |
} else { | |
vehicle.rotation.y = vehicle.userData.speed > 0 ? -Math.PI / 2 : Math.PI / 2; | |
} | |
scene.add(vehicle); | |
vehicles.push(vehicle); | |
} | |
} | |
function createPedestrians(count) { | |
const pedGeo = new THREE.CylinderGeometry(0.05, 0.05, 0.3, 8); | |
const pedMat = new THREE.MeshStandardMaterial({ color: 0x00aa00, roughness: 0.8, metalness: 0.1 }); | |
for (let i = 0; i < count; i++) { | |
const pedestrian = new THREE.Mesh(pedGeo, pedMat.clone()); | |
pedestrian.material.color.setHSL(Math.random(), 0.6, 0.6); | |
pedestrian.castShadow = true; | |
const buildingIndex = Math.floor(Math.random() * buildings.length); | |
const targetBuilding = buildings[buildingIndex]; | |
if (!targetBuilding) continue; | |
const side = Math.floor(Math.random() * 4); | |
let offsetX = 0, offsetZ = 0; | |
const actualBuildingWidth = targetBuilding.scale.x; // This is the inner facade width | |
const actualBuildingDepth = targetBuilding.scale.z; // This is the inner facade depth | |
const sidewalkGap = 0.35 + columnRadius; // Place pedestrians outside columns | |
if (side === 0) { | |
offsetZ = actualBuildingDepth / 2 + sidewalkGap; | |
offsetX = (Math.random() - 0.5) * actualBuildingWidth; | |
} else if (side === 1) { | |
offsetZ = -actualBuildingDepth / 2 - sidewalkGap; | |
offsetX = (Math.random() - 0.5) * actualBuildingWidth; | |
} else if (side === 2) { | |
offsetX = actualBuildingWidth / 2 + sidewalkGap; | |
offsetZ = (Math.random() - 0.5) * actualBuildingDepth; | |
} else { | |
offsetX = -actualBuildingWidth / 2 - sidewalkGap; | |
offsetZ = (Math.random() - 0.5) * actualBuildingDepth; | |
} | |
pedestrian.position.set( | |
targetBuilding.position.x + offsetX, // Relative to building center | |
0.15, | |
targetBuilding.position.z + offsetZ // Relative to building center | |
); | |
pedestrian.userData.speed = THREE.MathUtils.randFloat(0.005, 0.01); | |
pedestrian.userData.direction = new THREE.Vector3(Math.random()-0.5, 0, Math.random()-0.5).normalize(); | |
scene.add(pedestrian); | |
pedestrians.push(pedestrian); | |
} | |
} | |
function createBirds(count) { | |
const birdGeo = new THREE.SphereGeometry(0.1, 8, 6); | |
const birdMat = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.5 }); | |
for (let i = 0; i < count; i++) { | |
const bird = new THREE.Mesh(birdGeo, birdMat.clone()); | |
bird.material.color.setHex(Math.random() * 0xffffff); | |
bird.position.set( | |
(Math.random() - 0.5) * citySize * buildingSpacing * 0.8, | |
THREE.MathUtils.randFloat(buildingMaxHeight + 2, buildingMaxHeight + 10), | |
(Math.random() - 0.5) * citySize * buildingSpacing * 0.8 | |
); | |
bird.userData.speed = THREE.MathUtils.randFloat(0.02, 0.06); | |
bird.userData.phase = Math.random() * Math.PI * 2; | |
bird.userData.amplitudeY = THREE.MathUtils.randFloat(0.5, 2); | |
bird.userData.radius = THREE.MathUtils.randFloat(citySize * 0.2, citySize * 0.5); | |
bird.userData.angle = Math.random() * Math.PI * 2; | |
bird.userData.angleSpeed = THREE.MathUtils.randFloat(0.001, 0.005) * (Math.random() > 0.5 ? 1: -1); | |
scene.add(bird); | |
birds.push(bird); | |
} | |
} | |
// --- Animation Loop (Unchanged) --- | |
let lastWindowUpdateTime = 0; | |
const windowUpdateInterval = 200; | |
function animate(time) { | |
requestAnimationFrame(animate); | |
const currentTime = time || 0; | |
const delta = currentTime - (lastWindowUpdateTime || 0); | |
const timeOfDay = (currentTime * 0.00002) % 1; | |
const sunAngle = timeOfDay * Math.PI * 2; | |
sunLight.position.set( | |
Math.cos(sunAngle) * skyRadius, | |
Math.sin(sunAngle) * skyRadius * 0.7, | |
Math.sin(sunAngle - Math.PI / 4) * skyRadius | |
); | |
sunLight.intensity = Math.max(0, Math.sin(sunAngle)) * 1.8; | |
sunLight.visible = sunLight.intensity > 0.01; | |
const moonAngle = sunAngle + Math.PI; | |
moonLight.position.set( | |
Math.cos(moonAngle) * skyRadius * 0.9, | |
Math.sin(moonAngle) * skyRadius * 0.6, | |
Math.sin(moonAngle - Math.PI / 4) * skyRadius * 0.9 | |
); | |
moonLight.intensity = Math.max(0, Math.sin(moonAngle)) * 0.4; | |
moonLight.visible = moonLight.intensity > 0.01; | |
const dayFactor = Math.pow(Math.max(0, Math.sin(sunAngle)), 0.7); | |
ambientLight.intensity = dayFactor * 0.5 + 0.1; | |
scene.background = nightClearColor.clone().lerp(dayClearColor, dayFactor); | |
if (scene.fog) { | |
scene.fog.color = nightFogColor.clone().lerp(dayFogColor, dayFactor); | |
scene.fog.near = skyRadius * 0.2 * (1 - dayFactor * 0.5); | |
scene.fog.far = skyRadius * (1 - dayFactor * 0.3); | |
} else { | |
scene.fog = new THREE.Fog(scene.background, skyRadius * 0.2, skyRadius); | |
} | |
if (delta > windowUpdateInterval) { | |
lastWindowUpdateTime = currentTime; | |
buildings.forEach(building => { | |
building.userData.windows.forEach(win => { | |
if (Math.random() < 0.05) { | |
const isNight = dayFactor < 0.3; | |
const lightOnProb = isNight ? 0.6 : 0.15; | |
if (Math.random() < lightOnProb) { | |
win.material.emissive.setHex(0xffffaa); | |
win.material.emissiveIntensity = THREE.MathUtils.randFloat(0.5, 1.2); | |
win.material.opacity = 0.9; | |
} else { | |
win.material.emissive.setHex(0x000000); | |
win.material.emissiveIntensity = 0; | |
win.material.opacity = 0.65; | |
} | |
} | |
}); | |
}); | |
} | |
const cityBoundary = citySize * buildingSpacing / 2; | |
vehicles.forEach(v => { | |
if (v.userData.axis === 'x') { | |
v.position.x += v.userData.speed; | |
if (v.position.x > cityBoundary && v.userData.speed > 0) v.position.x = -cityBoundary; | |
if (v.position.x < -cityBoundary && v.userData.speed < 0) v.position.x = cityBoundary; | |
} else { | |
v.position.z += v.userData.speed; | |
if (v.position.z > cityBoundary && v.userData.speed > 0) v.position.z = -cityBoundary; | |
if (v.position.z < -cityBoundary && v.userData.speed < 0) v.position.z = cityBoundary; | |
} | |
}); | |
pedestrians.forEach(p => { | |
p.position.addScaledVector(p.userData.direction, p.userData.speed); | |
if (Math.random() < 0.01) { | |
p.userData.direction.set(Math.random()-0.5, 0, Math.random()-0.5).normalize(); | |
} | |
p.position.x = THREE.MathUtils.clamp(p.position.x, -cityBoundary * 1.1, cityBoundary * 1.1); | |
p.position.z = THREE.MathUtils.clamp(p.position.z, -cityBoundary * 1.1, cityBoundary * 1.1); | |
}); | |
birds.forEach(b => { | |
b.userData.angle += b.userData.angleSpeed; | |
b.position.x = Math.cos(b.userData.angle) * b.userData.radius; | |
b.position.z = Math.sin(b.userData.angle) * b.userData.radius; | |
b.position.y = (buildingMaxHeight + 5) + Math.sin(currentTime * 0.001 * b.userData.speed + b.userData.phase) * b.userData.amplitudeY; | |
}); | |
controls.update(); | |
renderer.render(scene, camera); | |
} | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
init(); | |
</script> | |
</body> | |
</html> | |