|
import streamlit as st |
|
import streamlit.components.v1 as components |
|
|
|
st.set_page_config(page_title="3D City Evolution Simulator", layout="wide") |
|
|
|
st.title("3D City Evolution Simulator") |
|
st.write("Watch a 3D city grow with lakes, hills, and evolving blocks") |
|
|
|
html_code = """ |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>3D City Evolution Simulator</title> |
|
<style> |
|
body { margin: 0; overflow: hidden; } |
|
#container { width: 100%; height: 100%; } |
|
canvas { width: 100%; height: 100%; display: block; } |
|
.ui-panel { |
|
position: absolute; |
|
top: 10px; |
|
right: 10px; |
|
background: rgba(0,0,0,0.7); |
|
padding: 15px; |
|
border-radius: 5px; |
|
color: white; |
|
font-family: Arial, sans-serif; |
|
z-index: 1000; |
|
} |
|
.ui-panel button { |
|
margin: 5px 0; |
|
padding: 5px 10px; |
|
width: 100%; |
|
background: #4CAF50; |
|
color: white; |
|
border: none; |
|
border-radius: 3px; |
|
cursor: pointer; |
|
} |
|
.ui-panel button:hover { background: #45a049; } |
|
</style> |
|
</head> |
|
<body> |
|
<div id="container"></div> |
|
<div class="ui-panel"> |
|
<h3>City Controls</h3> |
|
<button id="evolve">Evolve City</button> |
|
<button id="reset">Reset View</button> |
|
<div id="stats"> |
|
<p>Buildings: <span id="building-count">0</span></p> |
|
<p>Blocks: <span id="block-count">0</span></p> |
|
<p>Generation: <span id="generation">0</span></p> |
|
</div> |
|
</div> |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script> |
|
<script> |
|
class BuildingLSystem { |
|
constructor() { |
|
this.axiom = "F"; |
|
this.rules = { |
|
"F": ["F[+F]", "F[-F]", "FF", "F"], |
|
"+": ["+"], |
|
"-": ["-"], |
|
"[": ["["], |
|
"]": ["]"] |
|
}; |
|
this.angle = Math.PI / 6; |
|
} |
|
|
|
generate() { |
|
let result = this.axiom; |
|
for (let i = 0; i < 2; i++) { |
|
let newString = ""; |
|
for (let char of result) { |
|
if (this.rules[char]) { |
|
const possible = this.rules[char]; |
|
newString += possible[Math.floor(Math.random() * possible.length)]; |
|
} else { |
|
newString += char; |
|
} |
|
} |
|
result = newString; |
|
} |
|
return result; |
|
} |
|
|
|
build(scene, basePos, maxHeight) { |
|
let height = 0; |
|
const stack = []; |
|
let position = basePos.clone(); |
|
let direction = new THREE.Vector3(0, 1, 0); |
|
const structure = new THREE.Group(); |
|
|
|
for (let char of this.generate()) { |
|
switch(char) { |
|
case 'F': |
|
if (height < maxHeight) { |
|
const width = 0.8 + Math.random() * 0.4; |
|
const floorHeight = 1 + Math.random() * 2; |
|
const geo = new THREE.BoxGeometry(width, floorHeight, width); |
|
const mat = new THREE.MeshPhongMaterial({ |
|
color: new THREE.Color(0.5 + Math.random() * 0.5, |
|
0.5 + Math.random() * 0.5, |
|
0.5 + Math.random() * 0.5) |
|
}); |
|
const floor = new THREE.Mesh(geo, mat); |
|
floor.position.copy(position).add(new THREE.Vector3(0, floorHeight/2, 0)); |
|
structure.add(floor); |
|
position.y += floorHeight; |
|
height += floorHeight; |
|
} |
|
break; |
|
case '+': |
|
direction.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.angle); |
|
break; |
|
case '-': |
|
direction.applyAxisAngle(new THREE.Vector3(0, 0, 1), -this.angle); |
|
break; |
|
case '[': |
|
stack.push(position.clone()); |
|
break; |
|
case ']': |
|
if (stack.length > 0) position = stack.pop(); |
|
break; |
|
} |
|
} |
|
return structure; |
|
} |
|
} |
|
|
|
class CitySimulator { |
|
constructor() { |
|
this.blocks = []; |
|
this.blockSize = 10; |
|
this.maxBuildingsPerBlock = 5; |
|
this.generation = 0; |
|
this.lakeCenters = [ |
|
new THREE.Vector2(20, 20), |
|
new THREE.Vector2(-30, 10) |
|
]; |
|
} |
|
|
|
addBlock(scene, x, z) { |
|
const block = { |
|
position: new THREE.Vector2(x, z), |
|
buildings: [], |
|
maxHeight: this.isWaterfront(x, z) ? 15 : 8 |
|
}; |
|
this.blocks.push(block); |
|
this.evolveBlock(scene, block, true); |
|
} |
|
|
|
isWaterfront(x, z) { |
|
const pos = new THREE.Vector2(x, z); |
|
return this.lakeCenters.some(center => |
|
pos.distanceTo(center) < 15 && pos.distanceTo(center) > 5); |
|
} |
|
|
|
evolveBlock(scene, block, initial = false) { |
|
if (block.buildings.length < this.maxBuildingsPerBlock) { |
|
const lsystem = new BuildingLSystem(); |
|
const gridX = Math.floor(Math.random() * 3) - 1; |
|
const gridZ = Math.floor(Math.random() * 3) - 1; |
|
const basePos = new THREE.Vector3( |
|
block.position.x + gridX * 2, |
|
this.getTerrainHeight(block.position.x, block.position.y), |
|
block.position.y + gridZ * 2 |
|
); |
|
|
|
const building = lsystem.build(scene, basePos, block.maxHeight); |
|
if (this.isWaterfront(block.position.x, block.position.y)) { |
|
building.scale.set(1.5, 2, 1.5); |
|
} |
|
scene.add(building); |
|
block.buildings.push(building); |
|
} |
|
} |
|
|
|
evolve(scene) { |
|
this.generation++; |
|
if (this.blocks.length < 20) { |
|
const x = (Math.random() - 0.5) * 140; |
|
const z = (Math.random() - 0.5) * 80; |
|
if (!this.isInLake(x, z)) this.addBlock(scene, x, z); |
|
} |
|
this.blocks.forEach(block => this.evolveBlock(scene, block)); |
|
this.updateStats(); |
|
} |
|
|
|
getTerrainHeight(x, z) { |
|
return Math.sin(x * 0.05) * Math.cos(z * 0.05) * 5; |
|
} |
|
|
|
isInLake(x, z) { |
|
const pos = new THREE.Vector2(x, z); |
|
return this.lakeCenters.some(center => pos.distanceTo(center) < 10); |
|
} |
|
|
|
updateStats() { |
|
const totalBuildings = this.blocks.reduce((sum, block) => sum + block.buildings.length, 0); |
|
document.getElementById('building-count').textContent = totalBuildings; |
|
document.getElementById('block-count').textContent = this.blocks.length; |
|
document.getElementById('generation').textContent = this.generation; |
|
} |
|
} |
|
|
|
let scene, camera, renderer, controls; |
|
let city; |
|
|
|
function init() { |
|
const container = document.getElementById('container'); |
|
if (!container) { |
|
console.error('Container not found'); |
|
return; |
|
} |
|
|
|
// Scene |
|
scene = new THREE.Scene(); |
|
scene.background = new THREE.Color(0x87CEEB); |
|
|
|
// Camera |
|
camera = new THREE.PerspectiveCamera(75, 16 / 9, 0.1, 1000); |
|
camera.position.set(0, 50, 80); |
|
|
|
// Renderer |
|
renderer = new THREE.WebGLRenderer({ antialias: true }); |
|
renderer.setSize(window.innerWidth, window.innerHeight * (9/16)); |
|
container.appendChild(renderer.domElement); |
|
|
|
// Lights |
|
const ambientLight = new THREE.AmbientLight(0x404040); |
|
scene.add(ambientLight); |
|
const sun = new THREE.DirectionalLight(0xffffff, 0.8); |
|
sun.position.set(50, 50, 50); |
|
scene.add(sun); |
|
|
|
// Ground |
|
const groundGeo = new THREE.PlaneGeometry(160, 90, 32, 32); |
|
const groundMat = new THREE.MeshPhongMaterial({ color: 0x4a7043 }); |
|
const ground = new THREE.Mesh(groundGeo, groundMat); |
|
ground.rotation.x = -Math.PI / 2; |
|
ground.position.y = -0.1; |
|
scene.add(ground); |
|
|
|
// Lakes |
|
const lakeGeo = new THREE.CircleGeometry(10, 32); |
|
const lakeMat = new THREE.MeshPhongMaterial({ color: 0x4682b4 }); |
|
const lakeCenters = [new THREE.Vector2(20, 20), new THREE.Vector2(-30, 10)]; |
|
lakeCenters.forEach(center => { |
|
const lake = new THREE.Mesh(lakeGeo, lakeMat); |
|
lake.rotation.x = -Math.PI / 2; |
|
lake.position.set(center.x, 0.01, center.y); |
|
scene.add(lake); |
|
}); |
|
|
|
// Bridge |
|
const bridgeGeo = new THREE.BoxGeometry(5, 0.2, 15); |
|
const bridgeMat = new THREE.MeshPhongMaterial({ color: 0x808080 }); |
|
const bridge = new THREE.Mesh(bridgeGeo, bridgeMat); |
|
bridge.position.set(15, 0.2, 20); |
|
scene.add(bridge); |
|
|
|
// Controls |
|
controls = new THREE.OrbitControls(camera, renderer.domElement); |
|
controls.enableDamping = true; |
|
controls.target.set(0, 0, 0); |
|
|
|
// City |
|
city = new CitySimulator(); |
|
city.addBlock(scene, 0, 0); |
|
|
|
// Events |
|
window.addEventListener('resize', onWindowResize); |
|
document.getElementById('evolve').addEventListener('click', () => city.evolve(scene)); |
|
document.getElementById('reset').addEventListener('click', resetView); |
|
|
|
animate(); |
|
} |
|
|
|
function resetView() { |
|
camera.position.set(0, 50, 80); |
|
controls.target.set(0, 0, 0); |
|
controls.update(); |
|
} |
|
|
|
function onWindowResize() { |
|
const width = window.innerWidth; |
|
const height = width * (9/16); |
|
camera.aspect = 16 / 9; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(width, height); |
|
} |
|
|
|
function animate() { |
|
requestAnimationFrame(animate); |
|
controls.update(); |
|
renderer.render(scene, camera); |
|
} |
|
|
|
window.onload = init; |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
|
|
|
|
components.html(html_code, height=600) |
|
|
|
st.sidebar.title("3D City Evolution Simulator") |
|
st.sidebar.write(""" |
|
## How to Play |
|
|
|
Watch a 3D city evolve with lakes, hills, and building blocks. |
|
|
|
### Controls: |
|
- **Evolve City**: Grow the city |
|
- **Reset View**: Return to default view |
|
- **Left-click + drag**: Rotate |
|
- **Right-click + drag**: Pan |
|
- **Scroll**: Zoom |
|
|
|
### Features: |
|
- 16:9 play area |
|
- Blocks (10x10 units) with up to 5 buildings |
|
- Buildings evolve floor-by-floor using L-systems |
|
- Terrain with hills and lakes |
|
- Waterfront properties grow larger |
|
- Bridges connect land masses |
|
""") |