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