|
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") |
|
|
|
|
|
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> |
|
""" |
|
|
|
|
|
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 |
|
""") |