Spaces:
Sleeping
Sleeping
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 roads, vegetation, and dynamic weather") | |
# Sliders for container size with initial 3:4 aspect ratio | |
max_width = min(1200, st.session_state.get('window_width', 1200)) | |
max_height = min(1600, st.session_state.get('window_height', 1600)) | |
col1, col2 = st.columns(2) | |
with col1: | |
container_width = st.slider("Container Width (px)", 300, max_width, 768, step=50) | |
with col2: | |
container_height = st.slider("Container Height (px)", 400, max_height, 1024, step=50) | |
html_code = f""" | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>3D City Evolution Simulator</title> | |
<style> | |
body {{ margin: 0; overflow: hidden; }} | |
#container {{ width: {container_width}px; height: {container_height}px; margin: 0 auto; }} | |
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(); | |
let baseWidth = 1.5; | |
// Add small attached buildings horizontally | |
const attachCount = Math.floor(Math.random() * 3); // 0-2 attachments | |
for (let i = 0; i < attachCount; i++) {{ | |
const attachWidth = baseWidth * 0.6; | |
const attachHeight = 1 + Math.random() * 2; | |
const geo = new THREE.BoxGeometry(attachWidth, attachHeight, attachWidth); | |
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 attach = new THREE.Mesh(geo, mat); | |
const offsetX = (i + 1) * (baseWidth + attachWidth) * (Math.random() > 0.5 ? 1 : -1); | |
attach.position.set(position.x + offsetX, attachHeight / 2, position.z); | |
attach.castShadow = true; | |
structure.add(attach); | |
}} | |
for (let char of this.generate()) {{ | |
switch(char) {{ | |
case 'F': | |
if (height < maxHeight) {{ | |
const width = baseWidth * (1 - height / maxHeight); | |
const floorHeight = 2 + 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)); | |
floor.castShadow = true; | |
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.roads = []; | |
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) ? 20 : 12 | |
}}; | |
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); | |
}} | |
}} | |
addRoad(scene, start, end) {{ | |
const distance = start.distanceTo(end); | |
const roadGeo = new THREE.PlaneGeometry(2, distance); | |
const roadMat = new THREE.MeshPhongMaterial({{ color: 0x555555 }}); | |
const road = new THREE.Mesh(roadGeo, roadMat); | |
road.rotation.x = -Math.PI / 2; | |
const midPoint = start.clone().add(end).multiplyScalar(0.5); | |
road.position.set(midPoint.x, 0.02, midPoint.y); | |
road.lookAt(new THREE.Vector3(end.x, 0, end.y)); | |
road.receiveShadow = true; | |
scene.add(road); | |
this.roads.push(road); | |
}} | |
addVegetation(scene) {{ | |
const treeGeo = new THREE.ConeGeometry(1, 3, 8); | |
const treeMat = new THREE.MeshPhongMaterial({{ color: 0x228B22 }}); | |
const shrubGeo = new THREE.SphereGeometry(0.5, 8, 8); | |
const shrubMat = new THREE.MeshPhongMaterial({{ color: 0x32CD32 }}); | |
for (let i = 0; i < 10; i++) {{ | |
const x = (Math.random() - 0.5) * 90; | |
const z = (Math.random() - 0.5) * 120; | |
if (!this.isInLake(x, z)) {{ | |
const tree = new THREE.Mesh(treeGeo, treeMat); | |
tree.position.set(x, this.getTerrainHeight(x, z) + 1.5, z); | |
tree.castShadow = true; | |
scene.add(tree); | |
const shrub = new THREE.Mesh(shrubGeo, shrubMat); | |
shrub.position.set(x + 1, this.getTerrainHeight(x, z) + 0.5, z + 1); | |
shrub.castShadow = true; | |
scene.add(shrub); | |
}} | |
}} | |
}} | |
evolve(scene) {{ | |
this.generation++; | |
if (this.blocks.length < 20) {{ | |
const x = (Math.random() - 0.5) * 90; | |
const z = (Math.random() - 0.5) * 120; | |
if (!this.isInLake(x, z)) {{ | |
this.addBlock(scene, x, z); | |
if (this.blocks.length > 1) {{ | |
const lastBlock = this.blocks[this.blocks.length - 2]; | |
this.addRoad(scene, lastBlock.position, this.blocks[this.blocks.length - 1].position); | |
}} | |
}} | |
}} | |
this.blocks.forEach(block => this.evolveBlock(scene, block)); | |
this.addVegetation(scene); | |
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; | |
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 with 3:4 aspect ratio | |
camera = new THREE.PerspectiveCamera(75, 3 / 4, 0.1, 1000); | |
camera.position.set(0, 50, 60); | |
// Renderer | |
renderer = new THREE.WebGLRenderer({{ antialias: true }}); | |
renderer.setSize({container_width}, {container_height}); | |
renderer.shadowMap.enabled = true; | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
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); | |
sun.castShadow = true; | |
sun.shadow.mapSize.width = 1024; | |
sun.shadow.mapSize.height = 1024; | |
sun.shadow.camera.near = 0.5; | |
sun.shadow.camera.far = 500; | |
scene.add(sun); | |
// Ground with bump mapping | |
const groundGeo = new THREE.PlaneGeometry(1000, 1000, 32, 32); // Extended to horizon | |
const groundMat = new THREE.MeshPhongMaterial({{ | |
color: 0x4a7043, | |
bumpScale: 0.5, | |
shininess: 10 | |
}}); | |
const ground = new THREE.Mesh(groundGeo, groundMat); | |
ground.rotation.x = -Math.PI / 2; | |
ground.position.y = -0.1; | |
ground.receiveShadow = true; | |
// Simple bump map (noise) | |
const canvas = document.createElement('canvas'); | |
canvas.width = 256; | |
canvas.height = 256; | |
const ctx = canvas.getContext('2d'); | |
for (let x = 0; x < 256; x++) {{ | |
for (let y = 0; y < 256; y++) {{ | |
const grayValue = Math.random() * 255; // Renamed 'value' to 'grayValue' for clarity | |
ctx.fillStyle = 'rgb(' + grayValue + ',' + grayValue + ',' + grayValue + ')'; | |
ctx.fillRect(x, y, 1, 1); | |
}} | |
}} | |
const bumpTexture = new THREE.Texture(canvas); | |
bumpTexture.needsUpdate = true; | |
groundMat.bumpMap = bumpTexture; | |
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); | |
lake.receiveShadow = true; | |
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); | |
bridge.castShadow = true; | |
bridge.receiveShadow = true; | |
scene.add(bridge); | |
// Clouds | |
const cloudGeo = new THREE.SphereGeometry(5, 8, 8); | |
const cloudMat = new THREE.MeshPhongMaterial({{ color: 0xFFFFFF, opacity: 0.8, transparent: true }}); | |
for (let i = 0; i < 5; i++) {{ | |
const cloud = new THREE.Mesh(cloudGeo, cloudMat); | |
cloud.position.set( | |
(Math.random() - 0.5) * 200, | |
50 + Math.random() * 20, | |
(Math.random() - 0.5) * 200 | |
); | |
cloud.castShadow = true; | |
scene.add(cloud); | |
}} | |
// Controls | |
controls = new THREE.OrbitControls(camera, renderer.domElement); | |
controls.enableDamping = true; | |
controls.target.set(0, 0, 0); | |
// City | |
const 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, 60); | |
controls.target.set(0, 0, 0); | |
controls.update(); | |
}} | |
function onWindowResize() {{ | |
const width = {container_width}; | |
const height = {container_height}; | |
camera.aspect = 3 / 4; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(width, height); | |
}} | |
function animate() {{ | |
requestAnimationFrame(animate); | |
controls.update(); | |
renderer.render(scene, camera); | |
}} | |
window.onload = init; | |
</script> | |
</body> | |
</html> | |
""" | |
# Render the HTML component with dynamic size | |
components.html(html_code, width=container_width, height=container_height) | |
st.sidebar.title("3D City Evolution Simulator") | |
st.sidebar.write(""" | |
## How to Play | |
Watch a 3D city evolve with roads, vegetation, and dynamic weather. | |
### Controls: | |
- **Evolve City**: Grow the city | |
- **Reset View**: Return to default view | |
- **Left-click + drag**: Rotate | |
- **Right-click + drag**: Pan | |
- **Scroll**: Zoom | |
- **Sliders**: Adjust play area size | |
### Features: | |
- 3:4 initial play area (768x1024) | |
- Roads connect blocks | |
- Trees and shrubs added each evolution | |
- Extended green ground with bump mapping | |
- Clouds and sunlight with shadows | |
- Buildings with horizontal attachments | |
""") |