Spaces:
Running
Running
import streamlit as st | |
import os, base64, shutil, random | |
from pathlib import Path | |
def load_aframe_and_extras(): | |
return """ | |
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script> | |
<script src="https://unpkg.com/[email protected]/dist/aframe-event-set-component.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/aframe-volumetric-fog@latest/dist/aframe-volumetric-fog.min.js"></script> | |
<script> | |
AFRAME.registerComponent('draggable', { | |
init: function () { | |
this.el.setAttribute('class', 'raycastable'); | |
this.el.setAttribute('cursor-listener', ''); | |
this.dragHandler = this.dragMove.bind(this); | |
this.el.sceneEl.addEventListener('mousemove', this.dragHandler); | |
this.el.addEventListener('mousedown', this.onDragStart.bind(this)); | |
this.el.addEventListener('mouseup', this.onDragEnd.bind(this)); | |
this.camera = document.querySelector('[camera]'); | |
}, | |
remove: function () { | |
this.el.removeAttribute('cursor-listener'); | |
this.el.sceneEl.removeEventListener('mousemove', this.dragHandler); | |
}, | |
onDragStart: function (evt) { | |
this.isDragging = true; | |
this.el.emit('dragstart'); | |
}, | |
onDragEnd: function (evt) { | |
this.isDragging = false; | |
this.el.emit('dragend'); | |
}, | |
dragMove: function (evt) { | |
if (!this.isDragging) return; | |
var camera = this.camera.getObject3D('camera'); | |
var worldPos = new THREE.Vector3(); | |
camera.getWorldPosition(worldPos); | |
var worldDir = new THREE.Vector3(); | |
camera.getWorldDirection(worldDir); | |
var camX = worldPos.x; | |
var camY = worldPos.y; | |
var camZ = worldPos.z; | |
var dirX = worldDir.x; | |
var dirY = worldDir.y; | |
var dirZ = worldDir.z; | |
var t = -camY / dirY; | |
var moveX = camX + t * dirX; | |
var moveZ = camZ + t * dirZ; | |
this.el.setAttribute('position', moveX + " " + 0.0 + " " + moveZ); | |
} | |
}); | |
AFRAME.registerComponent('undulating-rotation', { | |
schema: { | |
amplitude: { type: 'number', default: 10 }, | |
frequency: { type: 'number', default: 0.3 } | |
}, | |
init: function () { | |
this.initialRotation = Math.random() * 360; | |
this.time = 0; | |
}, | |
tick: function (time, deltaTime) { | |
this.time += deltaTime / 1000; | |
const rotationAmount = Math.sin(this.time * this.data.frequency) * this.data.amplitude; | |
this.el.setAttribute('rotation', { x: 0, y: this.initialRotation + rotationAmount, z: 0 }); | |
} | |
}); | |
</script> | |
""" | |
def encode_file(file_path): | |
with open(file_path, "rb") as file: | |
return base64.b64encode(file.read()).decode() | |
def generate_tilemap(files, directory, grid_width, grid_height, bump_map_url, max_unique_models=5): | |
assets = "<a-assets>" | |
entities = "" | |
tile_size = 1 | |
start_x = -(grid_width * tile_size) / 2 | |
start_z = -(grid_height * tile_size) / 2 | |
available_models = [f for f in files if f.endswith(('.obj', '.glb'))] | |
available_images = [f for f in files if f.endswith(('.webp', '.png'))] | |
encoded_files = {} | |
unique_files = random.sample(files, min(len(files), max_unique_models)) | |
for file in unique_files: | |
file_path = os.path.join(directory, file) | |
file_type = file.split('.')[-1] | |
encoded_file = encode_file(file_path) | |
encoded_files[file] = encoded_file | |
if file_type in ['obj', 'glb']: | |
assets += f'<a-asset-item id="{Path(file).stem}" src="data:application/octet-stream;base64,{encoded_file}"></a-asset-item>' | |
elif file_type in ['webp', 'png']: | |
mime_type = f"image/{file_type}" | |
assets += f'<img id="{Path(file).stem}" src="data:{mime_type};base64,{encoded_file}" />' | |
assets += f""" | |
<a-material id="ground-mat" color="#78815d" normal-map="{bump_map_url}" normal-texture-repeat="{grid_width * 2} {grid_height * 2}"></a-material> | |
""" | |
assets += "</a-assets>" | |
for i in range(grid_width): | |
for j in range(grid_height): | |
x = start_x + (i * tile_size) | |
z = start_z + (j * tile_size) | |
position = f"{x} 0 {z}" | |
chosen_file = random.choice(files) | |
file_type = chosen_file.split('.')[-1] | |
file_stem = Path(chosen_file).stem | |
rotation = f"0 0 0" # Initial rotation can be 0 for the undulation to be clearer | |
amplitude = random.uniform(5, 15) | |
frequency = random.uniform(0.1, 0.5) | |
scale = "0.5 0.5 0.5" | |
if file_type == 'obj': | |
entities += f'<a-entity position="{position}" rotation="{rotation}" scale="{scale}" obj-model="obj: #{file_stem}" class="raycastable" draggable undulating-rotation="amplitude: {amplitude}; frequency: {frequency}"></a-entity>' | |
elif file_type == 'glb': | |
entities += f'<a-entity position="{position}" rotation="{rotation}" scale="{scale}" gltf-model="#{file_stem}" class="raycastable" draggable undulating-rotation="amplitude: {amplitude}; frequency: {frequency}"></a-entity>' | |
elif file_type in ['webp', 'png']: | |
if available_images: | |
random_image = random.choice(available_images) | |
image_stem = Path(random_image).stem | |
entities += f'<a-plane position="{position}" rotation="-90 0 0" width="{tile_size * 0.8}" height="{tile_size * 0.8}" material="src: #{image_stem}" class="raycastable" draggable></a-plane>' | |
return assets, entities | |
def main(): | |
st.set_page_config(layout="wide") | |
with st.sidebar: | |
st.markdown("### 🤖 3D AI Using Claude 3.5 Sonnet for AI Pair Programming") | |
st.markdown("[Open 3D Animation Toolkit](https://huggingface.co/spaces/awacke1/3d_animation_toolkit)", unsafe_allow_html=True) | |
st.markdown("### ⬆️ Upload") | |
uploaded_files = st.file_uploader("Add files:", accept_multiple_files=True, key="file_uploader") | |
st.markdown("### 🗺️ Grid Size") | |
grid_width = st.slider("Grid Width", 1, 10, 8) | |
grid_height = st.slider("Grid Height", 1, 10, 5) | |
st.markdown("### 🖼️ Bump Map") | |
bump_map_file = st.file_uploader("Upload Bump Map (png)", type=["png"], key="bump_map_uploader") | |
st.markdown("### ℹ️ Instructions") | |
st.write("- Click and drag to move objects") | |
st.write("- Mouse wheel to zoom") | |
st.write("- Right-click and drag to rotate view") | |
st.markdown("### 📁 Directory") | |
directory = st.text_input("Enter path:", ".", key="directory_input") | |
if not os.path.isdir(directory): | |
st.sidebar.error("Invalid directory path") | |
return | |
file_types = ['obj', 'glb', 'webp', 'png'] | |
if uploaded_files: | |
for uploaded_file in uploaded_files: | |
file_extension = Path(uploaded_file.name).suffix.lower()[1:] | |
if file_extension in file_types: | |
with open(os.path.join(directory, uploaded_file.name), "wb") as f: | |
shutil.copyfileobj(uploaded_file, f) | |
st.sidebar.success(f"Uploaded: {uploaded_file.name}") | |
else: | |
st.sidebar.warning(f"Skipped unsupported file: {uploaded_file.name}") | |
files = [f for f in os.listdir(directory) if f.split('.')[-1] in file_types] | |
bump_map_url = "" | |
if bump_map_file: | |
bump_map_bytes = bump_map_file.read() | |
bump_map_base64 = base64.b64encode(bump_map_bytes).decode() | |
bump_map_url = f"data:image/png;base64,{bump_map_base64}" | |
elif os.path.exists(os.path.join(directory, "bump_map.png")): # Optional: Check for a default bump map | |
bump_map_url = "bump_map.png" | |
aframe_scene = f""" | |
<a-scene embedded style="height: 600px; width: 100%;"> | |
{load_aframe_and_extras()} | |
<a-entity id="rig" position="0 {max(grid_width, grid_height) * 0.8} {max(grid_width, grid_height) * 0.8}" rotation="-45 0 0"> | |
<a-camera id="camera" look-controls wasd-controls="enabled: false" cursor="rayOrigin: mouse"></a-camera> | |
</a-entity> | |
<a-entity volumetric-fog="density: 0.02; color: #cccccc; near: 1; far: 50; animation: property: volumetric-fog.density; to: 0.04; dur: 8000; loop: true; dir: alternate"></a-entity> | |
<a-entity id="skybox"> | |
<a-plane position="0 0 -50" rotation="0 0 0" width="100" height="100" material="color: #87CEEB"></a-plane> | |
<a-plane position="0 0 50" rotation="0 180 0" width="100" height="100" material="color: #ADD8E6"></a-plane> | |
<a-plane position="50 0 0" rotation="0 90 0" width="100" height="100" material="color: #B0E0E6"></a-plane> | |
<a-plane position="-50 0 0" rotation="0 -90 0" width="100" height="100" material="color: #E0FFFF"></a-plane> | |
<a-plane position="0 50 0" rotation="-90 0 0" width="100" height="100" material="color: #ADD8E6"></a-plane> | |
<a-plane position="0 -50 0" rotation="90 0 0" width="100" height="100" material="color: #F0F8FF"></a-plane> | |
</a-entity> | |
<a-plane id="ground" position="0 -0.5 0" rotation="-90 0 0" width="{grid_width}" height="{grid_height}" material="src: #ground-mat"></a-plane> | |
<a-entity id="tilemap"> | |
{''.join(generate_tilemap(files, directory, grid_width, grid_height, bump_map_url))} | |
</a-entity> | |
<a-entity light="type: ambient; color: #445451"></a-entity> | |
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1 2"></a-entity> | |
</a-scene> | |
""" | |
st.components.v1.html(aframe_scene, height=600) | |
if __name__ == "__main__": | |
main() |