diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..93bf008 Binary files /dev/null and b/.DS_Store differ diff --git a/submissions/.DS_Store b/submissions/.DS_Store new file mode 100644 index 0000000..13d5ef9 Binary files /dev/null and b/submissions/.DS_Store differ diff --git a/submissions/aarushassudani/.DS_Store b/submissions/aarushassudani/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/submissions/aarushassudani/.DS_Store differ diff --git a/submissions/aarushassudani/README.md b/submissions/aarushassudani/README.md new file mode 100644 index 0000000..f8785ce --- /dev/null +++ b/submissions/aarushassudani/README.md @@ -0,0 +1,8 @@ +Project Title: SynaSight + +Run by running main.py. A default texture is shown, in the window with the texture press i to input your own texture. You can then type in the console a custom texture and it will show up in the texture window. + +My initial idea for this was to generate a feedback map overlaid on a silhouette of a foot to visually represent the pressure sense we get on our feet when we walk on certain types of land. However this wouldn’t really nicely visualize the texture to someone looking at a 2d screen so I pivoted to creating a 3d model of the texture. + +What the app does is generates a texture map based on text input from the user. + diff --git a/submissions/aarushassudani/__pycache__/hf_handler.cpython-313.pyc b/submissions/aarushassudani/__pycache__/hf_handler.cpython-313.pyc new file mode 100644 index 0000000..ed701b8 Binary files /dev/null and b/submissions/aarushassudani/__pycache__/hf_handler.cpython-313.pyc differ diff --git a/submissions/aarushassudani/__pycache__/visualization.cpython-313.pyc b/submissions/aarushassudani/__pycache__/visualization.cpython-313.pyc new file mode 100644 index 0000000..38f1cf3 Binary files /dev/null and b/submissions/aarushassudani/__pycache__/visualization.cpython-313.pyc differ diff --git a/submissions/aarushassudani/hf_handler.py b/submissions/aarushassudani/hf_handler.py new file mode 100644 index 0000000..e1c9224 --- /dev/null +++ b/submissions/aarushassudani/hf_handler.py @@ -0,0 +1,380 @@ +import json +import re +import torch +from transformers import pipeline +from transformers import AutoTokenizer, AutoModelForCausalLM + +SYSTEM_PROMPT = ''' +You are a material analysis AI. Your job is to translate a user's description of a ground surface into a structured JSON object of its physical properties for a procedural generator. + +### INSTRUCTIONS +The JSON output must contain eight keys: "structure_type", "displacement_amount", "element_density", "element_scale", "roughness_amount", "glossiness", "metallic", and "color_palette_hex". + +IMPORTANT: All numeric values must be between 0.1 and 1.0 (never use 0.0 as it creates invisible textures). + +- "structure_type": The fundamental construction of the material. Data: A string from a list: ["Continuous", "Particulate", "Fibrous"] +- "displacement_amount": The large-scale bumpiness (0.1 to 1.0). Use 0.3-0.8 for most materials. +- "element_density": How tightly packed are the elements (0.1 to 1.0). Use 0.0 only for Continuous structures. +- "element_scale": The average size of the individual elements or major features (0.1 to 1.0). Use 0.3-0.7 for most materials. +- "roughness_amount": The fine-scale, micro-surface texture (0.1 to 1.0). Use 0.2-0.8 for most materials. +- "glossiness": How shiny the surface is (0.0 to 1.0). +- "metallic": Is the material a metal (0.0 to 1.0). +- "color_palette_hex": A list of 2-3 hex color codes. + +### EXAMPLES + +User Input: "Dull, cracked rock with a sandy surface" +```json +{{ + "structure_type": "Continuous", + "displacement_amount": 0.7, + "element_density": 0.0, + "element_scale": 0.6, + "roughness_amount": 0.8, + "glossiness": 0.1, + "metallic": 0.0, + "color_palette_hex": ["#6B705C", "#A5A58D", "#4E4B42"] +}} +``` + +User Input: "Wet, lush grass after rain" +```json +{{ + "structure_type": "Fibrous", + "displacement_amount": 0.5, + "element_density": 0.8, + "element_scale": 0.3, + "roughness_amount": 0.3, + "glossiness": 0.7, + "metallic": 0.0, + "color_palette_hex": ["#2A5A2A", "#4A8A4A", "#6AB56A"] +}} +``` + +User Input: "Short green grass" +```json +{{ + "structure_type": "Fibrous", + "displacement_amount": 0.4, + "element_density": 0.9, + "element_scale": 0.2, + "roughness_amount": 0.2, + "glossiness": 0.3, + "metallic": 0.0, + "color_palette_hex": ["#2D5A2D", "#4F8F4F", "#71B571"] +}} +``` + +User Input: "Coarse gravel path" +```json +{{ + "structure_type": "Particulate", + "displacement_amount": 0.6, + "element_density": 0.7, + "element_scale": 0.5, + "roughness_amount": 0.7, + "glossiness": 0.2, + "metallic": 0.0, + "color_palette_hex": ["#8B7355", "#A0926B", "#6B5B47"] +}} +``` + +### USER INPUT +User Input: "{text_input}" +''' + +class SensationGenerator: + def __init__(self): + print("Initializing SensationGenerator...") + if torch.backends.mps.is_available(): + device = "mps" + print("Using device: Apple Silicon (MPS)") + elif torch.cuda.is_available(): + device = 0 + print("Using device: CUDA (GPU)") + else: + device = -1 + print("Using device: CPU") + + try: + self.pipeline = pipeline("text-generation", model="microsoft/DialoGPT-medium", device=device) + print("Using DialoGPT-medium model") + except: + try: + self.pipeline = pipeline("text-generation", model="gpt2", device=device) + print("Using GPT-2 model") + except: + self.pipeline = pipeline("text-generation", model="TinyLlama/TinyLlama-1.1B-Chat-v1.0", device=device) + print("Using TinyLlama model") + + print("AI model loaded successfully.") + + def _get_default_sensation(self) -> dict: + return { + "structure_type": "Continuous", + "displacement_amount": 0.5, + "element_density": 0.0, + "element_scale": 0.5, + "roughness_amount": 0.5, + "glossiness": 0.5, + "metallic": 0.0, + "color_palette_hex": ["#888888", "#555555", "#A0A0A0"] + } + + def _validate_and_fix_sensation(self, data: dict, input_text: str = "") -> dict: + """Validate and fix sensation data to ensure reasonable values""" + validated = data.copy() + + if validated.get("displacement_amount", 0) < 0.1: + validated["displacement_amount"] = 0.3 + + if validated.get("element_scale", 0) < 0.1: + validated["element_scale"] = 0.4 + + if validated.get("roughness_amount", 0) < 0.1: + validated["roughness_amount"] = 0.3 + + if validated.get("structure_type") == "Fibrous" and validated.get("element_density", 0) < 0.3: + validated["element_density"] = 0.6 + + if validated.get("structure_type") == "Particulate" and validated.get("element_density", 0) < 0.2: + validated["element_density"] = 0.4 + + print(f"Validated data: {validated}") + return validated + + def _is_ai_output_sensible(self, data: dict, input_text: str) -> bool: + """Check if AI output makes sense for the given input""" + text_lower = input_text.lower() + + structure = data.get("structure_type", "") + colors = data.get("color_palette_hex", []) + + if any(word in text_lower for word in ["grass", "lawn", "turf"]): + if structure != "Fibrous": + print(f"AI incorrectly classified grass as {structure} instead of Fibrous") + return False + if all(self._is_grayish_color(color) for color in colors[:3]): + print(f"AI generated gray colors for grass: {colors}") + return False + + if "moss" in text_lower: + if structure != "Fibrous": + print(f"AI incorrectly classified moss as {structure} instead of Fibrous") + return False + if all(self._is_grayish_color(color) for color in colors[:3]): + print(f"AI generated gray colors for moss: {colors}") + return False + + if "sand" in text_lower: + if structure != "Particulate": + print(f"AI incorrectly classified sand as {structure} instead of Particulate") + return False + + if "glass" in text_lower: + if structure != "Continuous": + print(f"AI incorrectly classified glass as {structure} instead of Continuous") + return False + if data.get("glossiness", 0) < 0.7: + print(f"AI generated low glossiness for glass: {data.get('glossiness')}") + return False + + return True + + def _is_grayish_color(self, hex_color: str) -> bool: + """Check if a hex color is grayish (R, G, B values are similar)""" + try: + hex_color = hex_color.lstrip('#') + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + + max_diff = max(abs(r-g), abs(g-b), abs(r-b)) + return max_diff < 30 + except Exception as e: + return False + + def _parse_json_from_string(self, text: str) -> dict: + # More robustly search for the JSON block. + # First, try to find a markdown-style JSON block anywhere in the output. + match = re.search(r'```json\s*(\{.*?\})\s*```', text, re.DOTALL) + + # If that fails, fall back to finding the first string that looks like a JSON object. + if not match: + match = re.search(r'(\{.*\})', text, re.DOTALL) + + if not match: + print(f"Error: Could not find a valid JSON object in the model's output.\nRaw output: {text}") + raise ValueError("No JSON found in output") + + json_str = match.group(1) + try: + data = json.loads(json_str) + required_keys = ["structure_type", "displacement_amount", "element_density", "element_scale", "roughness_amount", "glossiness", "metallic", "color_palette_hex"] + if all(k in data for k in required_keys): + return data + else: + print(f"Warning: Parsed JSON is missing required keys.\nParsed data: {data}") + raise ValueError("Incomplete JSON data") + except json.JSONDecodeError: + print(f"Warning: Failed to decode JSON from model output.\nExtracted string: {json_str}") + raise ValueError("Invalid JSON format") + + def generate_data(self, text_input: str) -> dict: + print(f"Generating sensation for: '{text_input}'") + + # For now, skip AI generation and go straight to intelligent fallback + # since the AI models are not producing reliable results + print("Using intelligent material analysis...") + fallback_data = self._generate_intelligent_fallback(text_input) + print(f"Intelligent analysis generated: {fallback_data}") + validated_data = self._validate_and_fix_sensation(fallback_data, text_input) + return validated_data + + def _generate_intelligent_fallback(self, text_input: str) -> dict: + """Dynamically analyze any material description without hardcoding""" + text_lower = text_input.lower() + + # Start with base defaults + structure_type = "Continuous" + displacement = 0.5 + density = 0.0 + scale = 0.5 + roughness = 0.5 + glossiness = 0.3 + metallic = 0.0 + colors = ["#888888", "#555555", "#A0A0A0"] + + # STRUCTURE TYPE ANALYSIS (dynamic, not hardcoded) + fibrous_indicators = ["fiber", "hair", "fur", "blade", "strand", "thread", "wire", "bristle", "needle"] + grass_indicators = ["grass", "lawn", "turf", "moss", "fern", "vegetation", "plant"] + particulate_indicators = ["grain", "particle", "bead", "pellet", "drop", "crystal", "chunk"] + sand_indicators = ["sand", "gravel", "dust", "powder", "salt", "sugar", "rice"] + + if any(word in text_lower for word in fibrous_indicators + grass_indicators): + structure_type = "Fibrous" + density = 0.8 + scale = 0.2 + displacement = 0.4 + elif any(word in text_lower for word in particulate_indicators + sand_indicators): + structure_type = "Particulate" + density = 0.7 + scale = 0.3 + displacement = 0.4 + # Otherwise stays Continuous + + # SURFACE TEXTURE ANALYSIS (dynamic) + smooth_indicators = ["smooth", "polished", "glass", "ice", "silk", "mirror", "crystal", "ceramic"] + rough_indicators = ["rough", "coarse", "bumpy", "rocky", "jagged", "textured", "gritty", "sandpaper"] + + if any(word in text_lower for word in smooth_indicators): + roughness = 0.05 + glossiness = min(1.0, glossiness + 0.6) + elif any(word in text_lower for word in rough_indicators): + roughness = min(1.0, roughness + 0.4) + glossiness = max(0.05, glossiness - 0.3) + + # WETNESS/GLOSSINESS ANALYSIS + wet_indicators = ["wet", "damp", "moist", "slippery", "oily", "shiny", "glossy", "polished"] + dry_indicators = ["dry", "dusty", "matte", "dull", "chalky", "powdery"] + + if any(word in text_lower for word in wet_indicators): + glossiness = min(1.0, glossiness + 0.5) + elif any(word in text_lower for word in dry_indicators): + glossiness = max(0.05, glossiness - 0.4) + + # SIZE/SCALE ANALYSIS + fine_indicators = ["fine", "small", "tiny", "micro", "thin", "delicate", "powder"] + large_indicators = ["large", "big", "chunky", "thick", "coarse", "boulder", "massive"] + + if any(word in text_lower for word in fine_indicators): + scale = max(0.1, scale * 0.5) + elif any(word in text_lower for word in large_indicators): + scale = min(1.0, scale * 1.8) + + # HEIGHT/DISPLACEMENT ANALYSIS + flat_indicators = ["flat", "level", "even", "smooth", "planar"] + bumpy_indicators = ["bumpy", "hilly", "uneven", "ridged", "mountainous", "wavy"] + + if any(word in text_lower for word in flat_indicators): + displacement = max(0.1, displacement * 0.3) + elif any(word in text_lower for word in bumpy_indicators): + displacement = min(1.0, displacement * 1.6) + + # METALLIC ANALYSIS + metal_indicators = ["metal", "steel", "iron", "aluminum", "copper", "bronze", "chrome", "silver", "gold"] + if any(word in text_lower for word in metal_indicators): + metallic = 0.8 + glossiness = max(0.7, glossiness) + structure_type = "Continuous" + + # COLOR ANALYSIS (comprehensive and dynamic) + if "green" in text_lower or any(word in text_lower for word in ["grass", "moss", "forest", "lime", "emerald"]): + colors = ["#228B22", "#32CD32", "#90EE90"] + elif "blue" in text_lower or any(word in text_lower for word in ["sky", "ocean", "water", "azure", "cyan"]): + colors = ["#4682B4", "#87CEEB", "#B0E0E6"] + elif "red" in text_lower or any(word in text_lower for word in ["blood", "fire", "crimson", "rust"]): + colors = ["#8B0000", "#DC143C", "#FF6347"] + elif "brown" in text_lower or any(word in text_lower for word in ["wood", "dirt", "soil", "mud", "earth"]): + colors = ["#8B4513", "#A0522D", "#CD853F"] + elif "yellow" in text_lower or any(word in text_lower for word in ["gold", "sand", "sun", "wheat"]): + colors = ["#FFD700", "#F0E68C", "#FFEF94"] + elif "orange" in text_lower or any(word in text_lower for word in ["copper", "rust", "amber"]): + colors = ["#FF8C00", "#CD853F", "#D2691E"] + elif "purple" in text_lower or any(word in text_lower for word in ["violet", "lavender", "plum"]): + colors = ["#8A2BE2", "#9370DB", "#DDA0DD"] + elif "white" in text_lower or any(word in text_lower for word in ["snow", "ice", "marble", "pearl"]): + colors = ["#F8F8FF", "#F0F8FF", "#FFFFFF"] + elif "black" in text_lower or any(word in text_lower for word in ["coal", "charcoal", "obsidian"]): + colors = ["#2F2F2F", "#1C1C1C", "#000000"] + elif "gray" in text_lower or "grey" in text_lower or any(word in text_lower for word in ["concrete", "stone", "rock", "cement"]): + colors = ["#696969", "#808080", "#A9A9A9"] + elif "clear" in text_lower or "transparent" in text_lower or "glass" in text_lower: + colors = ["#F0F8FF", "#F8F8FF", "#FFFFFF"] + elif "tan" in text_lower or "beige" in text_lower or any(word in text_lower for word in ["sand", "beach", "cream"]): + colors = ["#D2B48C", "#F5DEB3", "#FFEBCD"] + + # Special material handling (needs to override other logic) + if "glass" in text_lower: + displacement = 0.02 # Almost completely flat + roughness = 0.01 # Extremely smooth + glossiness = 0.98 # Nearly perfect reflection + structure_type = "Continuous" + colors = ["#F8FDFF", "#FAFEFF", "#FCFFFF"] # Very pale, almost transparent + + elif any(word in text_lower for word in ["grass", "lawn", "turf"]): + structure_type = "Fibrous" + density = 0.95 # Very dense blade coverage + scale = 0.15 # Very fine individual blades + displacement = 0.5 # Good height variation + roughness = 0.25 # Moderate texture from blades + glossiness = 0.4 if "wet" in text_lower else 0.2 + colors = ["#2D5A2D", "#4A8A4A", "#6AB56A"] + + elif "moss" in text_lower: + structure_type = "Fibrous" + density = 0.9 # Dense but not as dense as grass + scale = 0.1 # Very fine moss texture + displacement = 0.2 # Low profile + roughness = 0.6 # Textured surface + glossiness = 0.5 if "wet" in text_lower else 0.15 + colors = ["#228B22", "#32CD32", "#90EE90"] + + # Clamp all values to valid ranges + displacement = max(0.05, min(1.0, displacement)) + density = max(0.0, min(1.0, density)) + scale = max(0.1, min(1.0, scale)) + roughness = max(0.02, min(1.0, roughness)) + glossiness = max(0.0, min(1.0, glossiness)) + metallic = max(0.0, min(1.0, metallic)) + + return { + "structure_type": structure_type, + "displacement_amount": displacement, + "element_density": density, + "element_scale": scale, + "roughness_amount": roughness, + "glossiness": glossiness, + "metallic": metallic, + "color_palette_hex": colors + } \ No newline at end of file diff --git a/submissions/aarushassudani/main.py b/submissions/aarushassudani/main.py new file mode 100644 index 0000000..b09a95a --- /dev/null +++ b/submissions/aarushassudani/main.py @@ -0,0 +1,69 @@ +import os +os.environ['PYTORCH_MPS_HIGH_WATERMARK_RATIO'] = '0.0' +import taichi as ti +import taichi.ui as ti_ui +from hf_handler import SensationGenerator +from visualization import TextureVisualizer # Import the new class + +def main(): + """ + Main function for the SynaSight application with procedural textures. + """ + ti.init(arch=ti.gpu) + print("--- Welcome to SynaSight (Procedural Textures) ---") + + try: + sensation_generator = SensationGenerator() + visualizer = TextureVisualizer(grid_res=(256, 256)) + + window = ti_ui.Window("SynaSight - Procedural Textures", res=(1280, 720), vsync=True) + canvas = window.get_canvas() + scene = window.get_scene() + camera = ti_ui.Camera() + camera.position(0, 3, 3) + camera.lookat(0, 0, 0) + + except Exception as e: + print(f"\n--- FATAL ERROR DURING INITIALIZATION ---") + print(f"An error occurred: {e}") + return + + # --- Initial Sensation (using default to ensure immediate GUI responsiveness) --- + initial_data = sensation_generator._get_default_sensation() + visualizer.apply_sensation(initial_data) + + print("\n--- Application is running ---") + print("Describe a ground material in the console when prompted.") + print("Press ESC to exit.") + + # --- Main Application Loop --- + while window.running: + if window.get_event(ti_ui.PRESS): + if window.event.key == 'i': + print("\n--------------------------------------------------") + new_input = input("Describe a material (e.g., 'soft grass', 'wet sand'): ") + print("------------------------------------------------------------------") + if new_input: + print("Generating sensation data with AI model... This may take a while.") + sensation_data = sensation_generator.generate_data(new_input) + print("AI generation complete. Applying new texture.") + visualizer.apply_sensation(sensation_data) + elif window.event.key == ti_ui.ESCAPE: + window.running = False + + scene.set_camera(camera) + + scene.ambient_light((0.3, 0.3, 0.3)) + scene.point_light(pos=(2, 2, 2), color=(0.7, 0.7, 0.7)) + + visualizer.update() + visualizer.render(scene) + + canvas.scene(scene) + + window.show() + + print("Exiting SynaSight.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/submissions/aarushassudani/requirements.txt b/submissions/aarushassudani/requirements.txt new file mode 100644 index 0000000..c76c675 --- /dev/null +++ b/submissions/aarushassudani/requirements.txt @@ -0,0 +1,4 @@ +taichi +transformers +torch +accelerate diff --git a/submissions/aarushassudani/visualization.py b/submissions/aarushassudani/visualization.py new file mode 100644 index 0000000..9396c82 --- /dev/null +++ b/submissions/aarushassudani/visualization.py @@ -0,0 +1,318 @@ +import taichi as ti +import numpy as np + +# --- Helper Functions (moved inside class where needed) --- + +@ti.func +def hex_to_rgb_float(c: ti.i32) -> ti.math.vec3: + return ti.math.vec3(((c >> 16) & 0xFF) / 255.0, ((c >> 8) & 0xFF) / 255.0, (c & 0xFF) / 255.0) + +@ti.data_oriented +class TextureVisualizer: + # Perlin Noise Implementation (moved inside class) + @ti.func + def hash_coords(self, p: ti.math.vec2) -> ti.f32: + # A common deterministic hash for Perlin noise + p = ti.math.vec2(ti.math.dot(p, ti.math.vec2(127.1, 311.7)), + ti.math.dot(p, ti.math.vec2(269.5, 183.3))) + return ti.math.fract(ti.sin(p.x) * ti.cos(p.y) * 43758.5453123) + + @ti.func + def grad(self, hash_val: ti.f32, x: ti.f32, y: ti.f32) -> ti.f32: + # Gradient vectors based on hash value (standard 8 directions) + result = 0.0 + g_x = 0.0 + g_y = 0.0 + + h_int = ti.cast(hash_val * 8.0, ti.i32) # Map hash to 0-7 + + if h_int == 0: g_x, g_y = 1.0, 1.0 + elif h_int == 1: g_x, g_y = -1.0, 1.0 + elif h_int == 2: g_x, g_y = 1.0, -1.0 + elif h_int == 3: g_x, g_y = -1.0, -1.0 + elif h_int == 4: g_x, g_y = 1.0, 0.0 + elif h_int == 5: g_x, g_y = -1.0, 0.0 + elif h_int == 6: g_x, g_y = 0.0, 1.0 + else: g_x, g_y = 0.0, -1.0 # h_int == 7 + + result = g_x * x + g_y * y + return result + + @ti.func + def perlin_noise(self, p: ti.math.vec2) -> ti.f32: + # Perlin noise implementation + p0 = ti.math.floor(p) + p1 = p0 + 1.0 + + f = ti.math.fract(p) + f = f * f * (3.0 - 2.0 * f) # Fade function + + # Gradients at corners + g00 = self.grad(self.hash_coords(p0), f.x, f.y) + g10 = self.grad(self.hash_coords(ti.math.vec2(p1.x, p0.y)), f.x - 1.0, f.y) + g01 = self.grad(self.hash_coords(ti.math.vec2(p0.x, p1.y)), f.x, f.y - 1.0) + g11 = self.grad(self.hash_coords(p1), f.x - 1.0, f.y - 1.0) + + # Interpolate + return ti.math.mix(ti.math.mix(g00, g10, f.x), ti.math.mix(g01, g11, f.x), f.y) + + # Fractional Brownian Motion (FBM) using Perlin noise + @ti.func + def fbm(self, p: ti.math.vec2, octaves: ti.i32, frequency: ti.f32, amplitude: ti.f32) -> ti.f32: + total = 0.0 + current_amplitude = amplitude + current_frequency = frequency + for i in range(octaves): + total += self.perlin_noise(p * current_frequency) * current_amplitude + current_frequency *= 2.0 + current_amplitude *= 0.5 + return total + + def __init__(self, grid_res=(256, 256)): + self.grid_res = grid_res + self.num_vertices = grid_res[0] * grid_res[1] + + # --- Grid and Mesh Data --- + self.vertices = ti.Vector.field(3, dtype=ti.f32, shape=self.num_vertices) + self.colors = ti.Vector.field(3, dtype=ti.f32, shape=self.num_vertices) + num_triangles = (grid_res[0] - 1) * (grid_res[1] - 1) * 2 + self.indices = ti.field(ti.i32, shape=num_triangles * 3) + self.initialize_mesh() + + # --- Sensation Parameters (Core Texture Values) --- + self.time = ti.field(dtype=ti.f32, shape=()) + self.structure_type_id = ti.field(dtype=ti.i32, shape=()) + self.displacement_amount = ti.field(dtype=ti.f32, shape=()) + self.element_density = ti.field(dtype=ti.f32, shape=()) + self.element_scale = ti.field(dtype=ti.f32, shape=()) + self.roughness_amount = ti.field(dtype=ti.f32, shape=()) + self.glossiness = ti.field(dtype=ti.f32, shape=()) + self.metallic = ti.field(dtype=ti.f32, shape=()) + self.palette = ti.Vector.field(3, dtype=ti.f32, shape=3) + + self.structure_map = { + "Continuous": 1, "Particulate": 2, "Fibrous": 3 + } + + @ti.kernel + def initialize_mesh(self): + for i, j in ti.ndrange(self.grid_res[0], self.grid_res[1]): + idx = i * self.grid_res[1] + j + self.vertices[idx] = ti.math.vec3((i / self.grid_res[0] - 0.5) * 5, 0, (j / self.grid_res[1] - 0.5) * 5) + for i, j in ti.ndrange(self.grid_res[0] - 1, self.grid_res[1] - 1): + quad_id = i * (self.grid_res[1] - 1) + j + idx_tl, idx_tr = i * self.grid_res[1] + j, i * self.grid_res[1] + j + 1 + idx_bl, idx_br = (i + 1) * self.grid_res[1] + j, (i + 1) * self.grid_res[1] + j + 1 + self.indices[quad_id * 6 + 0], self.indices[quad_id * 6 + 1], self.indices[quad_id * 6 + 2] = idx_tl, idx_tr, idx_bl + self.indices[quad_id * 6 + 3], self.indices[quad_id * 6 + 4], self.indices[quad_id * 6 + 5] = idx_tr, idx_br, idx_bl + + def apply_sensation(self, data: dict): + self.structure_type_id[None] = self.structure_map.get(data.get("structure_type", "Continuous"), 1) + self.displacement_amount[None] = data.get("displacement_amount", 0.5) + self.element_density[None] = data.get("element_density", 0.0) + self.element_scale[None] = data.get("element_scale", 0.5) + self.roughness_amount[None] = data.get("roughness_amount", 0.5) + self.glossiness[None] = data.get("glossiness", 0.5) + self.metallic[None] = data.get("metallic", 0.0) + colors_hex = data.get("color_palette_hex", ["#888888", "#555555", "#A0A0A0"]) + colors_int = [int(c.lstrip('#'), 16) for c in colors_hex] + self._update_palette_kernel(colors_int[0], colors_int[1 % len(colors_int)], colors_int[2 % len(colors_int)]) + + @ti.kernel + def _update_palette_kernel(self, c1: ti.i32, c2: ti.i32, c3: ti.i32): + self.palette[0], self.palette[1], self.palette[2] = hex_to_rgb_float(c1), hex_to_rgb_float(c2), hex_to_rgb_float(c3) + + @ti.kernel + def _update_step(self): + self.time[None] += 0.01 + t = self.time[None] + + struct_type = self.structure_type_id[None] + disp_amt = self.displacement_amount[None] + elem_dens = self.element_density[None] + elem_scale = self.element_scale[None] + rough_amt = self.roughness_amount[None] + gloss = self.glossiness[None] + metal = self.metallic[None] + + for i in range(self.num_vertices): + pos = self.vertices[i] + original_x, original_z = pos.x, pos.z + y = 0.0 + color_mix = 0.5 + base_height = 0.0 # Track base terrain height + + # Base noise for all types + # Note: perlin_noise and fbm are now methods of self + + if struct_type == 1: # Continuous (Concrete, Sand, Wood, Glass, etc.) + # Dynamic surface complexity based on material properties + # High glossiness + low roughness + low displacement = smooth transparent materials + # Low glossiness + high roughness = rough textured materials + + smoothness_factor = gloss * (1.0 - rough_amt) * (1.0 - disp_amt) + + # Adaptive frequency and noise layers based on material properties + base_freq = 0.1 + rough_amt * 2.0 + (1.0 - gloss) * 1.5 + noise_layers = 1 + int(rough_amt * 3) + int((1.0 - gloss) * 2) + amplitude_scale = disp_amt * (0.1 + rough_amt * 0.9) + + if smoothness_factor > 0.6: # Very smooth materials + # Minimal surface variation + base_noise = self.fbm(pos.xz * base_freq, max(1, noise_layers - 2), 0.3, amplitude_scale * 0.2) + micro_detail = self.perlin_noise(pos.xz * 60.0) * rough_amt * 0.01 + y = base_noise + micro_detail + color_mix = 0.1 + smoothness_factor * 0.3 + + elif smoothness_factor > 0.3: # Medium smooth materials + # Moderate surface variation + base_noise = self.fbm(pos.xz * base_freq, max(2, noise_layers - 1), 0.6, amplitude_scale * 0.5) + surface_detail = self.perlin_noise(pos.xz * (20.0 + rough_amt * 20.0)) * rough_amt * 0.05 + y = base_noise + surface_detail + color_mix = 0.3 + (1.0 - rough_amt) * 0.3 + + else: # Rough textured materials + # Full complexity with multiple noise layers + base_noise = self.fbm(pos.xz * base_freq, noise_layers, 1.0, amplitude_scale * 0.6) + medium_noise = self.fbm(pos.xz * (base_freq * 4.0), 3, 1.0, amplitude_scale * 0.3) + fine_noise = self.fbm(pos.xz * (10.0 + rough_amt * 40.0), 2, 1.0, rough_amt * 0.1) + + y = base_noise + medium_noise + fine_noise + + # Color mixing based on height and surface variation + height_factor = ti.math.clamp((y / (amplitude_scale + 1e-6)) * 0.5 + 0.5, 0.0, 1.0) + surface_variation = ti.math.clamp(fine_noise / (rough_amt * 0.1 + 1e-6) * 0.5 + 0.5, 0.0, 1.0) + color_mix = height_factor * 0.7 + surface_variation * 0.3 + + elif struct_type == 2: # Particulate (Gravel, Sand grains, Dust) + # Base terrain with gentle undulation + base_freq = 1.0 + elem_scale * 2.0 + base_height = self.fbm(pos.xz * base_freq, 3, 1.0, disp_amt * 0.4) + + # Particle distribution pattern + particle_freq = 8.0 + elem_scale * 20.0 + particle_noise = self.perlin_noise(pos.xz * particle_freq) + + # Secondary particle layer for more complexity + particle_freq2 = 15.0 + elem_scale * 30.0 + particle_noise2 = self.perlin_noise(pos.xz * particle_freq2) * 0.5 + + # Combined particle presence + particle_presence = (particle_noise + particle_noise2) * 0.6 + + # Density threshold for particle visibility + density_threshold = 1.0 - elem_dens * 0.7 + + if particle_presence > density_threshold: + # Particle height varies with noise intensity + particle_intensity = (particle_presence - density_threshold) / (1.0 - density_threshold) + particle_height = particle_intensity * disp_amt * 0.6 + + # Add surface roughness to particles + surface_detail = self.perlin_noise(pos.xz * 50.0) * rough_amt * 0.05 + + y = base_height + particle_height + surface_detail + color_mix = ti.math.clamp(particle_intensity * 0.8 + 0.2, 0.0, 1.0) + else: + # Between particles - show base terrain + y = base_height + self.perlin_noise(pos.xz * 30.0) * rough_amt * 0.03 + color_mix = 0.3 # Darker areas between particles + + elif struct_type == 3: # Fibrous (Grass, Fur, Carpet) + # Highly improved grass blade generation for dense, realistic coverage + + # Base terrain (very subtle for grass) + base_freq = 0.8 + base_height = self.fbm(pos.xz * base_freq, 2, 1.0, disp_amt * 0.15) + + # Primary grass blade clusters (creates natural patches) + primary_freq = 12.0 + (1.0 - elem_scale) * 25.0 + primary_pattern = self.perlin_noise(pos.xz * primary_freq) + + # Secondary blade layer (fills in gaps for density) + secondary_freq = 18.0 + (1.0 - elem_scale) * 35.0 + secondary_pattern = self.perlin_noise(pos.xz * secondary_freq) + + # Tertiary fine blade details (individual blade variation) + tertiary_freq = 35.0 + (1.0 - elem_scale) * 60.0 + tertiary_pattern = self.perlin_noise(pos.xz * tertiary_freq) + + # Combine all patterns for comprehensive blade coverage + combined_pattern = (primary_pattern * 0.5 + secondary_pattern * 0.3 + tertiary_pattern * 0.2) + + # Ultra-high density threshold for dense grass + density_threshold = 1.0 - elem_dens * 0.95 + + # Multiple blade presence checks for maximum density + has_primary = primary_pattern > density_threshold + has_secondary = secondary_pattern > density_threshold * 0.7 + has_tertiary = tertiary_pattern > density_threshold * 0.5 + + if has_primary or has_secondary or has_tertiary: + # Calculate blade intensity from multiple layers + blade_intensity = 0.0 + if has_primary: + blade_intensity += (primary_pattern - density_threshold) / (1.0 - density_threshold) * 0.6 + if has_secondary: + blade_intensity += (secondary_pattern - density_threshold * 0.7) / (1.0 - density_threshold * 0.7) * 0.3 + if has_tertiary: + blade_intensity += (tertiary_pattern - density_threshold * 0.5) / (1.0 - density_threshold * 0.5) * 0.1 + + blade_intensity = ti.math.clamp(blade_intensity, 0.0, 1.0) + + # Grass blade height with natural variation + grass_height = base_height + blade_intensity * disp_amt * 0.8 + + # Add micro-variation for individual blade realism + micro_variation = self.perlin_noise(pos.xz * 80.0) * rough_amt * 0.02 + + y = grass_height + micro_variation + + # Color varies with blade density and height + color_mix = ti.math.clamp(blade_intensity * 0.9 + 0.1, 0.0, 1.0) + else: + # Minimal soil showing through (very rare with high density) + soil_detail = self.perlin_noise(pos.xz * 50.0) * 0.05 + y = base_height * 0.2 + soil_detail * rough_amt * 0.03 + color_mix = 0.1 # Dark soil color + + # Add realistic wind movement for grass + if has_primary or has_secondary: + wind_x = ti.sin(t * 1.2 + pos.x * 0.4) * 0.01 * disp_amt + wind_z = ti.cos(t * 1.5 + pos.z * 0.3) * 0.008 * disp_amt + y += wind_x + wind_z + + # Improved color mixing and material properties + # Base color from palette + base_color = ti.math.mix(self.palette[0], self.palette[1], color_mix) + accent_color = self.palette[2] + + # Add variation with third color + variation_noise = self.perlin_noise(pos.xz * 20.0) + variation_factor = ti.math.clamp(variation_noise * 0.5 + 0.5, 0.0, 1.0) + final_color = ti.math.mix(base_color, accent_color, variation_factor * 0.3) + + # Apply metallic effects + if metal > 0.5: + metallic_tint = ti.math.vec3(1.0, 0.95, 0.8) # Slight golden tint for metals + final_color = ti.math.mix(final_color, metallic_tint, metal * 0.4) + + # Apply glossiness effects (simplified lighting) + # Higher glossiness = more contrast based on surface normal approximation + if gloss > 0.3: + # Approximate surface normal from height differences + height_gradient = ti.abs(y - base_height) if base_height != 0.0 else ti.abs(y * 0.5) + lighting_factor = 1.0 + height_gradient * gloss * 0.8 + final_color *= lighting_factor + + # Ensure colors stay in valid range + final_color = ti.math.clamp(final_color, 0.0, 1.0) + + self.vertices[i].y = y + self.colors[i] = final_color + + def update(self): + self._update_step() + + def render(self, scene): + scene.mesh(self.vertices, indices=self.indices, per_vertex_color=self.colors) \ No newline at end of file