diff --git a/README.md b/README.md
new file mode 100644
index 0000000..df74f22
--- /dev/null
+++ b/README.md
@@ -0,0 +1,302 @@
+# ComfyUI-ShaderNoiseKSampler
+
+
+
+
+
+
+**Transform AI image generation from random sampling into deliberate navigation through mathematical topologies.**
+
+ComfyUI-ShaderNoiseKSampler is an advanced custom KSampler node that replaces standard noise generation with a multi-stage shader-based system. Unlike traditional sampling that teleports randomly between different seeds, this tool provides a structured vehicle for exploring the neighborhood around a chosen seed, using mathematical patterns to navigate along coherent paths through latent space.
+
+> [!TIP]
+> Perfect for artists and researchers looking to explore the latent space with more intention, sculpt image features with mathematical precision, and achieve unique visual styles through deliberate exploration rather than random discovery.
+
+
+*(Suggestion: Replace with a WEBP/GIF showcasing the ShaderNoiseKSampler in action, perhaps cycling through different noise types or parameters.)*
+
+## ๐งญ Navigating the Seed Universe
+
+Think of standard image generation, where you try different seeds, as driving from one town to another. Each new seed takes you to a completely different town.
+
+The ShaderNoiseKSampler works differently. It's like picking one town (one specific seed) that you find particularly interesting, and instead of just driving through to the next one, you decide to stop, get out of the car, and really explore *that specific town*. You can wander down its hidden alleyways, check out its diverse neighborhoods, and discover all the unique details and variations it holds.
+
+The shader noise is your map and the shader parameters (like Noise Scale, Octaves, Warp Strength, etc.) are your compass for this in-depth local exploration:
+
+- **๐ Noise Scale: The Zoom Control** - Determines how "zoomed in" or "zoomed out" you are in latent space
+- **๐ฌ Octaves: The Detail Slider** - Controls the level of detail and complexity in your noise pattern
+- **๐ Warp Strength: The Non-Linear Navigator** - Creates non-linear paths through latent space
+- **๐ Phase Shift: The Perspective Shifter** - Reveals different "facets" of the same core elements
+
+The core innovation is treating latent space as a territory to be explored rather than a lottery to be played - turning the act of AI image generation into a journey of deliberate artistic discovery guided by the elegant language of mathematical patterns.
+
+## ๐ฎ The Innovation: Controllable Noise
+
+While traditional samplers rely on pure randomness, **ShaderNoiseKSampler** introduces a paradigm shift: mathematically controllable noise patterns derived from shader technology (shader noise). This isn't just navigation - it's the invention of a new vehicle.
+
+By replacing the standard random noise distribution with structured shader-generated patterns (shader noise), we transform the diffusion process from a random walk into a deliberate journey. The noise itself becomes an artistic medium you can sculpt through mathematical parameters:
+
+- Scale, octaves, and warp create precise noise topographies
+- Transformations and blending modes give you noise "vocabulary"
+- Shape masks and color schemes provide spatial and frequency control
+
+This controlled noise approach bridges the gap between the deterministic world of procedural generation and the probabilistic nature of diffusion models, offering creative control without sacrificing the generative AI's creative potential.
+
+## ๐ The Shader Matrix: In-Depth Documentation
+
+A core feature of this project is the **"๐ Show Shader Matrix"** button available on the `ShaderNoiseKSampler` (and related) nodes. Clicking this button opens an extensive, self-contained modal dialogโThe Shader Matrixโwhich provides:
+
+- Detailed explanations of all shader noise types, mathematical foundations, and parameters.
+- Interactive visualizations of noise patterns and shape masks.
+- Python code examples for noise generation.
+- A comprehensive guide to the philosophy and usage of the node.
+
+This README provides an overview, but the Shader Matrix is your ultimate guide for deep dives!
+
+## โจ Features
+
+
+*(Suggestion: Replace with a screenshot of the ShaderNoiseKSampler node interface.)*
+
+- **๐ Advanced KSampler Replacement**: Integrates directly into your workflow as a KSampler.
+- **๐ฌ Multi-Stage Shader Application**:
+ - **Sequential Stages**: Apply shader noise over segments of the diffusion process.
+ - **Injection Stages**: Apply shader noise at specific, discrete steps.
+- **๐จ Twelve Shader Noise Archetypes**: Explore a vast range of patterns:
+ - Tensor Field, Cellular, Domain Warp, Fractal (FBM), Perlin, Waves, Gaussian, Heterogeneous FBM, Interference, Spectral, 3D Projection, Curl Noise.
+- **๐ญ Sophisticated Blending & Transformations**:
+ - **Blend Modes**: Combine shader noise with base noise using modes like Multiply, Add, Overlay, Screen, Soft Light, Hard Light, Difference.
+ - **Noise Transformations**: Apply mathematical operations (Absolute, Sin, Square Root, etc.) to shader noise before blending.
+- **๐ Shape Masks**: Spatially modulate noise with geometric overlays (Radial, Linear, Grid, Vignette, Spiral, Hexgrid, etc.) with adjustable strength.
+- **๐ Color Schemes Integration**: Apply color transformations (Inferno, Magma, Viridis, Jet, Turbo, etc.) to the noise *before* it influences the diffusion model, subtly guiding structure and aesthetics. Adjustable intensity.
+- **โณ Temporal Coherence**:
+ - Generate frame-consistent evolving noise for animations.
+ - Ensure consistent base noise for predictable exploration when tweaking parameters for still images.
+- **๐๏ธ Granular Control**:
+ - Global `shader_strength` and per-stage strength tuning.
+ - Adjust `noise_scale`, `octaves`, `warp_strength`, `phase_shift`, and more for each stage.
+- **๐พ Parameter Management**: Save your shader parameter configurations.
+- **๐ "Show Shader Matrix" Button**: Access comprehensive, interactive documentation and visualizations directly within ComfyUI (Alt+M shortcut).
+- **๐ค Compatibility**: Works with various models including SD 1.5, SDXL, WAN2.1, Hunyuan, and more (see Model Compatibility section).
+- **๐ง Latent Space Cartography**: Create a map of the territory surrounding your seed, developing an intuitive understanding of how to navigate to specific effects.
+- **๐ Persistent Identities in Variation**: Observe how similar elements persist across parameter adjustments, revealing how the model encodes concepts and their relationships.
+- **๐ Discovery of "Hidden Gems"**: Find interesting variations that exist in the spaces "between" seeds that random sampling might statistically miss.
+
+## ๐ฅ Installation
+
+### Option 1: Using ComfyUI Manager
+
+1. Install [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) if you don't have it already.
+2. Open ComfyUI, go to the **Manager** tab.
+3. Click on **Install Custom Nodes**.
+4. Search for "**ComfyUI-ShaderNoiseKSampler**" and click **Install**.
+5. Restart ComfyUI.
+
+### Option 2: Manual Installation
+
+```bash
+cd /path/to/ComfyUI/custom_nodes
+git clone https://github.com/AEmotionStudio/ComfyUI-ShaderNoiseKsampler.git
+```
+
+Restart ComfyUI after installation. No additional `pip install` steps are required for the core functionality.
+
+## ๐ Usage
+
+1. **Add Node**: Add the `ShaderNoiseKSampler` node to your ComfyUI graph.
+2. **Connect Inputs**:
+ - `model`: Your primary AI model.
+ - `positive`, `negative`: Your conditioning prompts.
+ - `latent_image`: The input latent (e.g., from an Empty Latent Image node).
+3. **Basic Sampling Parameters**:
+ - Set `seed`, `steps`, `cfg`, `sampler_name`, `scheduler`, and `denoise` as you would for a standard KSampler.
+4. **Configure Shader Noise**: This is where the exploration begins!
+ - **Stages**: Define `sequential_stages` and `injection_stages`.
+ - **Global Controls**: Set `shader_strength` (0.0 to disable shaders), `blend_mode`, and `noise_transform`.
+ - **Per-Stage Controls**: For each stage, configure:
+ - `shader_noise_type` (e.g., perlin, cellular)
+ - `noise_scale` (zoom control), `noise_octaves` (detail level), `noise_warp_strength` (non-linear navigation), `noise_phase_shift` (perspective shift)
+ - `shape_mask_type` and `shape_mask_strength`
+ - `color_scheme` and `color_intensity`
+ - Stage-specific `strength_multiplier`
+ - **Temporal Coherence**: Enable `use_temporal_coherence` for animations or consistent exploration.
+5. **Explore**: Use the "๐ Show Shader Matrix" button (or Alt+M) to better understand the noise patterns you're creating.
+6. **Generate**: Queue your prompt and witness the shader-guided generation!
+
+> [!TIP]
+> Start with a low `shader_strength` (e.g., 0.1-0.3) and a single `sequential_stage` to understand the impact of different noise types and parameters. Gradually increase complexity as you become more familiar with how each parameter navigates the latent space.
+
+## ๐ง Latent Space Navigation
+
+Unlike random seed exploration, the ShaderNoiseKSampler provides a methodical way to navigate the latent space:
+
+- **Creative Control vs. Serendipity**: Find a balance between intentional direction and unexpected discovery. You're not precisely controlling the output, but you're not completely at the mercy of randomness either - you're steering through possibility space.
+
+- **Frequency Domain Exploration**: The mathematical nature of this tool allows you to explore how different frequency patterns map to semantic features in the generated images, revealing fundamental patterns in how visual information is encoded in the model.
+
+- **Methodical Discovery**: As you experiment with different parameter combinations, you'll develop an intuitive understanding of how they affect the output, allowing for more deliberate creative choices.
+
+- **Fine-Grained Control**: You can introduce a wide spectrum of changes, from barely noticeable subtleties to more significant transformations. This gives you the flexibility to choose your desired degree of alteration, all while preserving the core meaning, motion, and recognizable objects (semantic elements) of the image.
+
+## โ๏ธ Configuration Options
+
+The `ShaderNoiseKSampler` offers extensive control. Key parameters are listed below with their navigational significance. For an exhaustive list and explanations, please refer to the **"๐ Show Shader Matrix"** documentation within ComfyUI.
+
+| Option | Description & Navigational Significance | Default (Example) |
+|------------------------------|-------------------------------------------------------------------------------------------------------------|-------------------|
+| **`seed`** | Master seed for reproducibility. This is your "town" in the latent space universe. | `8888` |
+| **`steps`** | Number of sampling iterations. | `20` |
+| **`cfg`** | Classifier-Free Guidance scale. | `7.0` |
+| **`sampler_name`** | E.g., `euler_ancestral`, `dpm_2_ancestral`. | `euler_ancestral` |
+| **`scheduler`** | E.g., `normal`, `beta`, `simple`. | `beta` |
+| **`denoise`** | Denoising strength. | `1.0` |
+| **`sequential_stages`** | Number of shader stages applied sequentially. | `1` |
+| **`injection_stages`** | Number of shader stages injected at specific steps. | `0` |
+| **`shader_strength`** | Global strength of shader noise influence (0.0 to disable). | `0.3` |
+| **`blend_mode`** | How shader noise combines with base noise (e.g., `multiply`, `add`). | `multiply` |
+| **`noise_transform`** | Math operation on shader noise (e.g., `none`, `absolute`, `sin`). | `none` |
+| **`use_temporal_coherence`** | For consistent noise in animations or exploration. | `false` |
+| **`shader_noise_type (per stage)`** | The base pattern (e.g., `domain_warp`, `tensor_field`, `curl_noise`). | `domain_warp` |
+| **`noise_scale (per stage)`** | The "Zoom Control" - determines how "zoomed in" or "zoomed out" you are in latent space. | `1.0` |
+| **`noise_octaves (per stage)`** | The "Detail Slider" - controls the level of detail and complexity in your noise pattern. | `1` |
+| **`noise_warp_strength (per stage)`** | The "Non-Linear Navigator" - creates non-linear paths through latent space. | `0.5` |
+| **`noise_phase_shift (per stage)`** | The "Perspective Shifter" - reveals different "facets" of the same core elements. | `0.5` |
+| **`shape_mask_type (per stage)`** | Geometric mask overlay (e.g., `radial`, `grid`). | `none` |
+| **`shape_mask_strength (per stage)`**| Intensity of the shape mask. | `0.5` |
+| **`color_scheme (per stage)`** | Color mapping for noise (e.g., `viridis`, `jet`). | `none` |
+| **`color_intensity (per stage)`**| Strength of the color scheme influence. | `0.8` |
+
+### Sampler & Scheduler Compatibility
+
+Generally, `ShaderNoiseKSampler` aims for broad compatibility. The following are often good starting points (refer to the Shader Matrix for more details):
+
+- **Recommended Samplers**: `euler_ancestral`, `dpm_2_ancestral`, `dpmppm_2_ancestral`, `lcm`
+- **Recommended Schedulers**: `beta` (often preferred), `normal`, `simple`, `kl_optimal`
+
+### ๐งฑ Model Compatibility
+
+The `ShaderNoiseKSampler` also aims to be compatible with a variety of models. Compatibility often depends on the model's latent channel structure. The node attempts to automatically detect the channel count for many common models.
+
+| Model Family / Name | Category | Status | Channels (Expected) | Notes |
+| ---------------------------------- | ------------ | ------------------------------------ | ------------------- | --------------------------------------------------------------------- |
+| SD 1.5 / SD_X4 | Image | Compatible | 4 | Standard 4-channel models. |
+| SDXL / SDXL Refiner | Image | Compatible | 4 | Standard 4-channel models. |
+| Stable Cascade B | Image | Compatible | 4 | Formerly "Stable Cascade (Decoder/Non-Prior)". |
+| Stable Cascade (Prior) | Image | Compatible | 16 | Higher channel count. |
+| SD3 | Image | Compatible | 16 | Higher channel count. |
+| Flux.1 / Flux | Image | Compatible | 16 | Higher channel count. |
+| HunyuanDiT | Image | Compatible | 4 | Standard 4-channel model, similar to SD1.5/SDXL. |
+| Chroma | Image | Compatible | 4 | Assumed 4-channel VAE based on related models. |
+| HiDream / Flow | Image | Compatible | 16 | Higher channel count. |
+| WAN / Warp / Pixel / Anime (WAN2.1) | Video (3D) | Compatible | 16 | Higher channel count. |
+| Mochi | Video (3D) | Compatible | 12 | May require specific handling. |
+| LTXV | Video (3D) | Compatible | 128 | Very high channel count. Special configuration or specific noise handling might be beneficial. |
+| CosmosVideo / Cosmos1CV8x8x8 | Video (3D) | Compatible | 16 | Channel count confirmed. |
+| HunyuanVideo | Video (3D) | Compatible | 16 | Official docs mention a 16x VAE channel compression. Effective channels for noise may differ or require specific handling. |
+| AnimateDiff (on SD 1.5) | Video (3D) | Compatible | 4 | Uses 4 channels from the base SD 1.5 model. |
+| Stable Video Diffusion (SVD) | Video (3D) | Compatible | 4 | Latent diffusion model, likely 4 channels. |
+| ACEStep / ACE | Audio (1D) | Compatible | 8 | Compatibility with 2D noise may vary. |
+| StableAudio1 | Audio (1D) | Untested | 64 | Compatibility with 2D noise is uncertain. |
+| Hunyuan3Dv2 | 3D Model | Untested | 64 | 1D channels. Compatibility with 2D noise is uncertain. |
+| Hunyuan3Dv2mini | 3D Model | Untested | 64 | 1D channels. Compatibility with 2D noise is uncertain. |
+| **Other Models** | | | | |
+| Generic 4-channel models | Image | Compatible (by default) | 4 | Most other standard diffusion models. |
+| Models with >16 channels (unlisted)| Various | Untested / May Require Configuration | Varies | Models with very high channel counts may require specific configurations or might exhibit different performance characteristics. |
+| Other non-standard architecture | Various | Untested | Varies | Results may vary. |
+
+**Note on Channel Detection:** The `ShaderNoiseKSampler` includes logic to infer model channel counts (see `get_model_channel_count` in `shader_noise_ksampler.py`). If you encounter issues with a new or untested model, understanding its latent channel structure is important. For models with very high channel counts, performance or compatibility might vary, and specific noise generation approaches could be more suitable.
+
+## ๐ฌ Shader Noise Deep Dive (Brief Overview)
+
+The true depth of `ShaderNoiseKSampler` lies in its components. The "Shader Matrix" covers these extensively.
+
+- **Shader Noise Types**: Start your exploration with three powerful, freely available noise archetypes: `Domain Warp` for intricate, flowing distortions, `Tensor Field` for structured and directional patterns, and `Curl Noise` for smooth, fluid dynamics. Each offers a **unique visual lens** ๐ญ for navigating latent space. โจ **Additional advanced noise types** are available to supporters on [Ko-fi](https://ko-fi.com/aemotionstudio).
+ * `[Image: Domain Warp Noise Preview]`
+ * `[Image: Tensor Field Noise Preview]`
+ * `[Image: Curl Noise Preview]`
+- **Blend Modes**: Determine how the crafted shader noise interacts with the underlying base noise. `Multiply` can create depth, `Add` can introduce highlights, and `Overlay` can enhance contrast.
+- **Noise Transformations**: Apply mathematical functions like `absolute` (creates ridges), `sin` (creates bands), or `sqrt` (compresses highlights) to the raw shader noise before blending, dramatically altering its characteristics.
+- **Shape Masks**: Impose geometric forms onto your noise. A `radial` mask can create focus, a `grid` can introduce blocky structures. Strength is key.
+- **Color Schemes**: More than just a visual flair for the noise preview, these schemes (`viridis`, `inferno`, `jet`, etc.) transform the noise data itself. This "colored" noise can then guide the diffusion model in unique ways, influencing texture, features, and mood by altering how the model "perceives" the noise structure.
+
+## โ Troubleshooting
+
+- **Shader Effects Not Visible**:
+ - Ensure `shader_strength` is greater than `0.0`.
+
+- **Unexpected Results**: Small parameter changes can sometimes lead to large visual shifts. Use the shader visualizer to understand the noise before generating. Experimentation is encouraged.
+
+- **Consult the Shader Matrix**: The in-app documentation is your best friend for detailed troubleshooting and understanding.
+
+### Known Issues
+
+- **Optimal Use Cases**: The `ShaderNoiseKSampler` currently demonstrates its strongest capabilities in text-to-video and text-to-image generation. Performance with image-to-video or video-to-video workflows may not be as pronounced.
+
+- **Parameter Queuing with Non-Direct KSampler**: The standard (non-direct) `ShaderNoiseKSampler` node does not support queuing of differing parameter sets for consecutive runs i.e. you can't save over older parameters because their read at runtime. For rapid iteration and experimentation with varied parameters without needing to save each configuration, the `(direct) ShaderNoiseSampler` variant is recommended.
+
+- > [!WARNING]
+ > **Potential for Visual Instability**: Certain parameter explorations, particularly with high intensity or complex interactions, may result in visually disruptive outputs such as flashing images or harsh artifacts. Users are advised to iterate with caution.
+
+## ๐ฑ Research & Development Nature
+
+ComfyUI-ShaderNoiseKSampler is born from dedicated personal research and investigation. While it introduces exciting ways to navigate latent space, please consider it an active exploration. This means there's a vast potential for further improvements, new discoveries, and community-driven enhancements as the project evolves. Your understanding of its current research-driven phase is appreciated!
+
+### ๐บ๏ธ Roadmap: The Next Evolution
+
+The journey into mathematically guided latent space exploration is just beginning. Here's a glimpse of what's on the horizon:
+
+| Area | Focus | Technologies Involved (Examples) |
+| :---------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- |
+| **๐ Advanced Latent Space Cartography** | Evolve current research into an intuitive "Visual Intent Engine". This system will allow users to express desired visual outcomes semantically (e.g., "enhance fabric texture," "shift lighting ambiance"). The engine will then intelligently translate these intents into optimal shader parameter configurations, fostering a more direct and expressive artistic workflow by deeply mapping the interplay between parameters and visual impact. | Semantic AI, Parameter Response Modeling, Machine Learning |
+| **๐จ Expanded Shader Noise Palettes** | Introduce a richer vocabulary of shader noise types, each with unique navigational properties and aesthetic fingerprints. Explore new dimensions of textural complexity, flow dynamics, and structural organization. | Advanced Noise Algorithms, GLSL concepts |
+| **๐งญ Precision Navigation Tools** | Create more granular and predictable tools for manipulating latent pathways. Research direct correlations between mathematical noise constructs and emergent visual features for greater artistic intent. | Mathematical Modelling, Latent Space Analysis |
+| **๐ฎ Cross-Modal Exploration** | Investigate applying structured noise principles to modalities beyond 2D images, such as 3D and audio, opening new avenues for creative exploration. | Signal Processing, Generative Models for Audio/3D |
+
+Stay tuned to unlock new ways to interact with and understand the vast potential of generative models through the lens of controlled, mathematical noise.
+
+## ๐ค Contributing
+
+Contributions are welcome! Whether it's bug reports, feature suggestions, or pull requests, your help is appreciated.
+
+1. Fork the repository.
+2. Create a new branch (`git checkout -b feature/AmazingFeature`).
+3. Make your changes.
+4. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
+5. Push to the branch (`git push origin feature/AmazingFeature`).
+6. Open a Pull Request.
+
+## ๐ Acknowledgements
+
+- The ComfyUI team for creating such a flexible and powerful platform.
+- The developers of libraries and concepts that inspired aspects of this work (e.g., GLSL, various noise algorithms).
+- The ComfyUI community for their continuous innovation and support.
+- Users and contributors who provide feedback and suggestions.
+
+## ๐ Connect with รmotion (Developer)
+
+- YouTube: [AEmotionStudio](https://www.youtube.com/@aemotionstudio/videos)
+- GitHub: [AEmotionStudio](https://github.com/AEmotionStudio)
+- Discord: [Join our community](https://discord.gg/UzC9353mfp)
+- Website: [aemotionstudio.org](https://aemotionstudio.org/)
+
+## โ Support
+
+If you find ComfyUI-ShaderNoiseKSampler useful and wish to support its development, consider:
+
+[](https://ko-fi.com/aemotionstudio)
+
+Your support helps dedicate more time to maintaining and improving this project, developing new features, and creating better documentation and tutorials.
+
+### ๐ Additional Ways to Support
+
+- โญ Star the repository on GitHub.
+- ๐ข Share it with others in the AI art community.
+- ๐ ๏ธ Contribute to its development (see Contributing section).
+- ๐ก Provide feedback and feature requests.
+
+For business inquiries or professional support, please contact me through my [website](https://aemotionstudio.org/) or join my [Discord server](https://discord.gg/UzC9353mfp).
+
+## ๐ License
+
+This project is licensed under the **GNU General Public License v3.0**.
+See the `LICENSE` file for details (You may need to create this file if it doesn't exist, containing the GPL-3.0 license text).
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..3b86841
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,76 @@
+# First import components from shader noise ksampler
+from .shader_noise_ksampler import (
+ ShaderNoiseKSampler,
+ register_shader_generator,
+ SHADER_GENERATORS
+)
+
+# Import DirectShaderNoiseKSampler
+from .direct_shader_ksampler import DirectShaderNoiseKSampler
+
+# Import the stripped version of ShaderNoiseKSampler
+# Commented out due to missing file
+# from .shader_noise_ksampler_stripped import ShaderNoiseKSamplerStripped
+
+# Next import tensor class
+from .shader_to_tensor import ShaderToTensor
+
+# Import and integrate domain warp
+from .shaders.domain_warp import add_domain_warp_to_tensor, generate_domain_warp_tensor
+
+# Apply domain warp integration
+print("=== INTEGRATING DOMAIN WARP TO SHADER SYSTEM ===")
+add_domain_warp_to_tensor(ShaderToTensor)
+register_shader_generator("domain_warp", generate_domain_warp_tensor)
+print(f"Domain warp integration complete - has domain_warp: {'domain_warp' in dir(ShaderToTensor)}")
+
+# Import and integrate tensor field
+from .shaders.tensor_field import add_tensor_field_to_tensor, generate_tensor_field_tensor
+
+# Apply tensor field integration
+print("=== INTEGRATING TENSOR FIELD TO SHADER SYSTEM ===")
+add_tensor_field_to_tensor(ShaderToTensor)
+register_shader_generator("tensor_field", generate_tensor_field_tensor)
+print(f"Tensor field integration complete - has tensor_field: {'tensor_field' in dir(ShaderToTensor)}")
+
+# Import and integrate Curl Noise
+from .shaders.curl_noise import add_curl_noise_to_tensor, generate_curl_noise_tensor
+
+# Apply Curl Noise integration
+print("=== INTEGRATING CURL NOISE TO SHADER SYSTEM ===")
+add_curl_noise_to_tensor(ShaderToTensor)
+register_shader_generator("curl", generate_curl_noise_tensor)
+register_shader_generator("curl_noise", generate_curl_noise_tensor)
+print(f"Curl noise integration complete - has curl_noise: {'curl_noise' in dir(ShaderToTensor)}")
+
+# Import and integrate temporal coherent noise
+from .shaders.temporal_coherent_noise import integrate_temporal_coherent_noise, generate_temporal_coherent_noise_tensor
+
+# Apply temporal coherent noise integration
+print("=== INTEGRATING TEMPORAL COHERENT NOISE TO SHADER SYSTEM ===")
+integrate_temporal_coherent_noise()
+register_shader_generator("temporal_coherent", generate_temporal_coherent_noise_tensor)
+register_shader_generator("temporal_coherent_noise", generate_temporal_coherent_noise_tensor)
+print(f"Temporal coherent noise integration complete - has temporal_coherent_noise: {'temporal_coherent_noise' in dir(ShaderToTensor)}")
+
+# Node class mappings
+NODE_CLASS_MAPPINGS = {
+ "ShaderNoiseKSampler": ShaderNoiseKSampler,
+ "ShaderNoiseKSamplerDirect": DirectShaderNoiseKSampler,
+}
+
+# Display name mappings
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "ShaderNoiseKSampler": "Shader Noise KSampler",
+ "ShaderNoiseKSamplerDirect": "Shader Noise KSampler (Direct)",
+}
+
+# Add web directory for UI components
+WEB_DIRECTORY = "./web"
+
+# List of JS files to be loaded
+__js_files__ = ["gradient_title.js", "shader_renderer.js", "shader_params_save_button.js", "shader_info_button.js", "noise_visualizer.js"]
+
+
+# List of exported elements
+__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY", "__js_files__", "__css_files__", "SHADER_GENERATORS"]
\ No newline at end of file
diff --git a/data/shader_params.json b/data/shader_params.json
new file mode 100644
index 0000000..cb2c8f3
--- /dev/null
+++ b/data/shader_params.json
@@ -0,0 +1,11 @@
+{
+ "shaderType": "curl_noise",
+ "shaderScale": 1,
+ "shaderOctaves": 1,
+ "shaderWarpStrength": 0.5,
+ "shaderShapeType": "none",
+ "shaderShapeStrength": 1,
+ "shaderPhaseShift": 0.5,
+ "colorScheme": "none",
+ "shaderColorIntensity": 0.8
+}
\ No newline at end of file
diff --git a/direct_shader_ksampler.py b/direct_shader_ksampler.py
new file mode 100644
index 0000000..a11c533
--- /dev/null
+++ b/direct_shader_ksampler.py
@@ -0,0 +1,299 @@
+import comfy.sample
+from .shader_params_reader import get_shader_params
+from .shader_noise_ksampler import ShaderNoiseKSampler, get_visualizer, set_debug_level
+
+# Define a simple parameter response mapper if needed
+class ParameterResponseMapper:
+ """Simplified version of ParameterResponseMapper for direct shader parameters"""
+ def __init__(self, model_type="SD1.5"):
+ self.model_type = model_type
+
+ def get_recommended_adjustments(self, target_attrs, current_params, max_params=2):
+ """Return recommended parameter adjustments based on target attributes"""
+ # This is a simple implementation that could be expanded
+ adjustments = {}
+
+ if not target_attrs or not isinstance(target_attrs, dict):
+ return adjustments
+
+ # Simple mapping from attributes to shader parameters
+ for attr, value in target_attrs.items():
+ # Only adjust if the attribute is strongly present (value > 0.5)
+ if value < 0.5:
+ continue
+
+ # Map common attributes to shader parameters
+ if attr == "detailed" and "octaves" in current_params:
+ adjustments["octaves"] = min(current_params["octaves"] + 1, 8)
+ elif attr == "smooth" and "octaves" in current_params:
+ adjustments["octaves"] = max(current_params["octaves"] - 1, 1)
+ elif attr == "twisted" and "warp_strength" in current_params:
+ adjustments["warp_strength"] = min(current_params["warp_strength"] + 0.2, 5.0)
+ elif attr == "flowing" and "phase_shift" in current_params:
+ adjustments["phase_shift"] = (current_params["phase_shift"] + 0.5) % 6.28
+ elif attr == "large_scale" and "scale" in current_params:
+ adjustments["scale"] = min(current_params["scale"] + 0.5, 10.0)
+ elif attr == "small_scale" and "scale" in current_params:
+ adjustments["scale"] = max(current_params["scale"] - 0.5, 0.1)
+
+ # Limit number of adjustments
+ if len(adjustments) >= max_params:
+ break
+
+ return adjustments
+
+class DirectShaderNoiseKSampler(ShaderNoiseKSampler):
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "model": ("MODEL", {"tooltip": "The AI model used for image generation"}),
+ "seed": ("INT", {"default": 8888, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Random seed for generation. Same seed with same parameters will generate the same image."}),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000, "tooltip": "Number of sampling steps. Higher values can produce better results but take longer"}),
+ "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.1, "tooltip": "Classifier-free guidance scale. Higher values follow the prompt more closely"}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler_ancestral", "tooltip": "Algorithm used for the sampling process"}),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "beta", "tooltip": "Scheduler used to determine noise level at each step"}),
+ "positive": ("CONDITIONING", {"tooltip": "Positive conditioning/prompts that guide what to include in the image"}),
+ "negative": ("CONDITIONING", {"tooltip": "Negative conditioning/prompts that guide what to exclude from the image"}),
+ "latent_image": ("LATENT", {"tooltip": "Input latent image to be processed"}),
+ "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Denoising strength. Lower values preserve more of the original image"}),
+ "sequential_stages": ("INT", {"default": 1, "min": 0, "max": 10, "step": 1, "tooltip": "Number of sequential shader stages to apply before injection stages"}),
+ "injection_stages": ("INT", {"default": 0, "min": 0, "max": 10, "step": 1, "tooltip": "Number of injection shader stages to apply after sequential stages"}),
+ "shader_strength": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Strength of the shader noise influence for both stage types. Set to 0.0 to disable and use only base noise."}),
+ "blend_mode": (["normal", "add", "multiply", "screen", "overlay", "soft_light", "hard_light", "difference"], {"default": "multiply", "tooltip": "Method used to blend the shader noise with the base noise"}),
+ "noise_transform": (["none", "reverse", "inverse", "absolute", "square", "sqrt", "log", "sin", "cos"], {"default": "none", "tooltip": "Apply mathematical transformations to the noise for creative effects"}),
+ "use_temporal_coherence": ("BOOLEAN", {"default": False, "tooltip": "Ensures consistent noise patterns. For sequences like video frames, it helps maintain frame-to-frame consistency (e.g., using the same base seed and 4D noise). For single image generations, it ensures the base noise is derived consistently from the main seed."}),
+
+ # New direct shader parameters
+ "shader_type": (["domain_warp", "tensor_field", "curl_noise"], {"default": "domain_warp", "tooltip": "Select the type of shader noise pattern to use in the visualization [different types have different characteristic outputs]"}),
+ "shape_type": (["none", "radial", "linear", "spiral", "checkerboard", "spots", "hexgrid", "stripes", "gradient", "vignette", "cross", "stars", "triangles", "concentric", "rays", "zigzag"], {"default": "none", "tooltip": "Apply a shape mask to the shader noise pattern to create more complex structures [not post processing - is applied to shader noise pattern before rendering]"}),
+ "color_scheme": (["none", "blue_red", "viridis", "plasma", "inferno", "magma", "turbo", "jet", "rainbow", "cool", "hot", "parula", "hsv", "autumn", "winter", "spring", "summer", "copper", "pink", "bone", "ocean", "terrain", "neon", "fire"], {"default": "none", "tooltip": "Choose a color palette to apply to the shader noise visualization [not post processing - is applied to the shader noise pattern before rendering]"}),
+ "noise_scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.001, "tooltip": "Adjust the scale of the shader noise pattern - lower values create larger, zoomed-in features; higher values create smaller, zoomed-out features [small value shifts can lead to larger variations]"}),
+ "octaves": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 8.0, "step": 0.1, "tooltip": "Number of shader noise layers to combine - higher values add more detail and complexity [small value shifts can lead to larger variations]"}),
+ "warp_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.001, "tooltip": "Control how much the shader noise pattern warps and distorts - higher values create more swirling or complex transformations [small adjustments are good for subtle variations]"}),
+ "shape_mask_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.0001, "tooltip": "Adjust the intensity of the shape mask\'s effect on the shader noise pattern - higher values make the shape more prominent [small adjustments are good for subtle variations - not effective without shape mask]"}),
+ "phase_shift": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 2.0, "step": 0.0001, "tooltip": "Shift the phase of the shader noise pattern to create different variations or animate patterns over time [small adjustments are good for subtle variations]"}),
+ "color_intensity": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.0001, "tooltip": "Adjust the intensity of the color scheme application - lower values are more desaturated, higher values are more vibrant [small adjustments are good for subtle variations - not effective without color scheme]"}),
+ },
+ "optional": {
+ "custom_sigmas": ("SIGMAS", {"tooltip": "Optional custom sigma schedule to override the model's default schedule"}),
+ },
+ "hidden": {
+ "debug_level": (["0-Off", "1-Basic", "2-Detailed", "3-Verbose"], {"default": "0-Off", "tooltip": "Enable debugging at specified level to understand what's happening during shader generation and sampling."}),
+ "fast_high_channel_noise": ("BOOLEAN", {"default": False, "tooltip": "Use a faster, simplified noise generation method for models with many channels (>16), like LTXV."}),
+ "sequential_distribution": (["uniform", "linear_decrease", "linear_increase", "gaussian", "first_stronger", "last_stronger"], {"default": "linear_decrease", "tooltip": "How shader strength is distributed across sequential stages"}),
+ "injection_distribution": (["uniform", "linear_decrease", "linear_increase", "gaussian", "first_stronger", "last_stronger"], {"default": "linear_decrease", "tooltip": "How shader strength is distributed across injection stages"}),
+ "denoise_visualization_frequency": (["Every step", "25% intervals", "10% intervals", "4 steps", "2 steps"], {"default": "Every step", "tooltip": "How often to save images during the denoising process. Higher frequency means more images but slower generation."}),
+ "target_attribute_changes": ("STRING", {"forceInput": True, "tooltip": "Connect output from ParameterResponseMapperNode here"}),
+ }
+ }
+
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "sample"
+ CATEGORY = "sampling"
+
+ @classmethod
+ def REGISTER_MATRIX_BUTTON(s):
+ return True
+
+ @classmethod
+ def IS_CHANGED(s, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
+ denoise=1.0, sequential_stages=2, injection_stages=3, shader_strength=0.3, blend_mode="multiply",
+ noise_transform="none", sequential_distribution="linear_decrease", injection_distribution="linear_decrease",
+ use_temporal_coherence=False, debug_level="0-Off", fast_high_channel_noise=False,
+ denoise_visualization_frequency="25% intervals", custom_sigmas=None, target_attribute_changes="",
+ shader_type="domain_warp", shape_type="none", color_scheme="none", noise_scale=1.0, octaves=1,
+ warp_strength=0.5, shape_mask_strength=1.0, phase_shift=0.5, color_intensity=0.8):
+ # Ensure all direct parameters also trigger re-execution when changed
+ return (seed, steps, cfg, sampler_name, scheduler, denoise, sequential_stages,
+ injection_stages, shader_strength, blend_mode, noise_transform,
+ sequential_distribution, injection_distribution, use_temporal_coherence,
+ debug_level, denoise_visualization_frequency, custom_sigmas,
+ target_attribute_changes, fast_high_channel_noise,
+ # Include direct shader parameters
+ shader_type, shape_type, color_scheme, noise_scale, octaves,
+ warp_strength, shape_mask_strength, phase_shift, color_intensity)
+
+ def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
+ denoise=1.0, sequential_stages=2, injection_stages=3, shader_strength=0.3, blend_mode="multiply",
+ noise_transform="none", sequential_distribution="linear_decrease", injection_distribution="linear_decrease",
+ use_temporal_coherence=False, debug_level="0-Off", fast_high_channel_noise=False,
+ denoise_visualization_frequency="25% intervals", custom_sigmas=None, target_attribute_changes="",
+ shader_type="tensor_field", shape_type="none", color_scheme="none", noise_scale=1.0, octaves=3.0,
+ warp_strength=0.5, shape_mask_strength=1.0, phase_shift=0.0, color_intensity=0.8):
+ """
+ Run the multi-stage shader noise k-sampler with direct parameter inputs
+ """
+ # Parse debug level from the selected option
+ debug_level_value = int(debug_level.split("-")[0])
+
+ # Set the debug level in the shader debugger
+ debugger = set_debug_level(debug_level_value)
+
+ # Get the visualizer
+ visualizer = get_visualizer()
+
+ # Get device early from latent_image
+ device = latent_image["samples"].device
+ if debugger.enabled:
+ print(f"โน๏ธ Using device: {device}")
+
+ # Get the shader parameters, but we'll modify them with our direct inputs
+ shader_params = get_shader_params()
+
+ # Override shader parameters with direct inputs - ensure all naming variants are set
+ # Shader Type (set all variants)
+ shader_params["shader_type"] = shader_type
+ shader_params["shaderType"] = shader_type
+
+ # Shape Type (set all variants)
+ shader_params["shape_type"] = shape_type
+ shader_params["shaderShapeType"] = shape_type
+
+ # Color Scheme
+ shader_params["colorScheme"] = color_scheme
+ shader_params["color_scheme"] = color_scheme
+
+ # Noise Scale (set all variants)
+ shader_params["scale"] = noise_scale
+ shader_params["shaderScale"] = noise_scale
+
+ # Octaves (set all variants)
+ shader_params["octaves"] = float(octaves) # Ensure float for compatibility
+ shader_params["shaderOctaves"] = float(octaves)
+
+ # Warp Strength (set all variants)
+ shader_params["warp_strength"] = warp_strength
+ shader_params["shaderWarpStrength"] = warp_strength
+
+ # Shape Mask Strength (set all variants)
+ shader_params["shapemaskstrength"] = shape_mask_strength
+ shader_params["shaderShapeStrength"] = shape_mask_strength
+ shader_params["shapeMaskStrength"] = shape_mask_strength # Added capital M version
+ shader_params["shape_mask_strength"] = shape_mask_strength # Added underscore version
+ shader_params["shape_strength"] = shape_mask_strength # Added alternative name checked in shader_to_tensor.py
+
+ # Phase Shift (set all variants)
+ shader_params["phase_shift"] = phase_shift
+ shader_params["shaderPhaseShift"] = phase_shift
+
+ # Color Intensity (set all variants)
+ shader_params["intensity"] = color_intensity
+ shader_params["shaderColorIntensity"] = color_intensity
+
+ # Other parameters
+ shader_params["time"] = shader_params.get("time", 0.0) # Keep existing time or set to 0
+ shader_params["base_seed"] = seed # Set base seed for temporal coherence
+ shader_params["useTemporalCoherence"] = use_temporal_coherence
+ shader_params["temporal_coherence"] = use_temporal_coherence # Add alternative name
+ shader_params["fast_high_channel_noise"] = fast_high_channel_noise
+
+ # Visualization type (default to 3/ellipses as in the default parameters)
+ shader_params["visualization_type"] = shader_params.get("visualization_type", 3)
+
+ # --- Handle Parameter Response Mapper Integration ---
+ if target_attribute_changes and target_attribute_changes.strip():
+ if debugger.enabled:
+ print("๐ง Applying Parameter Response Mapper adjustments...")
+ try:
+ import json
+ target_attrs = json.loads(target_attribute_changes)
+
+ if isinstance(target_attrs, dict) and target_attrs:
+ # Determine model type for mapper (simple inference)
+ model_name_lower = getattr(model, 'model_name', "").lower()
+ if "sdxl" in model_name_lower:
+ mapper_model_type = "SDXL"
+ else:
+ mapper_model_type = "SD1.5" # Default
+
+ # Use our local ParameterResponseMapper
+ mapper = ParameterResponseMapper(model_type=mapper_model_type)
+
+ # Extract current shader parameters relevant to the mapper
+ current_mapper_params = {
+ "scale": noise_scale,
+ "octaves": float(octaves),
+ "warp_strength": warp_strength,
+ "phase_shift": phase_shift
+ }
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f" Mapper using model_type: {mapper_model_type}")
+ print(f" Target Attributes: {target_attrs}")
+ print(f" Current Params for Mapper: {current_mapper_params}")
+
+ # Get recommendations
+ recommendations = mapper.get_recommended_adjustments(
+ target_attrs,
+ current_mapper_params,
+ max_params=2
+ )
+
+ if recommendations:
+ if debugger.enabled:
+ print(f"โ Mapper recommended adjustments: {recommendations}")
+ # Apply recommendations to the shader_params - set ALL variants
+ for param, value in recommendations.items():
+ # Update all possible parameter naming variants
+ if param == "scale":
+ shader_params["scale"] = value
+ shader_params["shaderScale"] = value
+ elif param == "octaves":
+ shader_params["octaves"] = float(value)
+ shader_params["shaderOctaves"] = float(value)
+ elif param == "warp_strength":
+ shader_params["warp_strength"] = value
+ shader_params["shaderWarpStrength"] = value
+ elif param == "phase_shift":
+ shader_params["phase_shift"] = value
+ shader_params["shaderPhaseShift"] = value
+ else:
+ # For any other parameters
+ shader_params[param] = value
+ except Exception as e:
+ print(f"โ Error processing target_attribute_changes: {e}. Skipping mapper adjustments.")
+
+ if debugger.enabled:
+ # Debug output for direct parameters
+ print(f"๐ง Direct Shader Parameters:")
+ print(f" Shader Type: {shader_type}")
+ print(f" Shape Type: {shape_type}")
+ print(f" Color Scheme: {color_scheme}")
+ print(f" Noise Scale: {noise_scale}")
+ print(f" Octaves: {octaves}")
+ print(f" Warp Strength: {warp_strength}")
+ print(f" Shape Mask Strength: {shape_mask_strength}")
+ print(f" Phase Shift: {phase_shift}")
+ print(f" Color Intensity: {color_intensity}")
+
+ # Call parent class sample method with the modified shader params
+ # Use the parent class implementation from ShaderNoiseKSampler
+ result = super().sample(
+ model=model,
+ seed=seed,
+ steps=steps,
+ cfg=cfg,
+ sampler_name=sampler_name,
+ scheduler=scheduler,
+ positive=positive,
+ negative=negative,
+ latent_image=latent_image,
+ denoise=denoise,
+ sequential_stages=sequential_stages,
+ injection_stages=injection_stages,
+ shader_strength=shader_strength,
+ blend_mode=blend_mode,
+ noise_transform=noise_transform,
+ sequential_distribution=sequential_distribution,
+ injection_distribution=injection_distribution,
+ use_temporal_coherence=use_temporal_coherence,
+ debug_level=debug_level,
+ fast_high_channel_noise=fast_high_channel_noise,
+ denoise_visualization_frequency=denoise_visualization_frequency,
+ custom_sigmas=custom_sigmas,
+ target_attribute_changes=target_attribute_changes,
+ shader_params_override=shader_params, # Pass our modified shader params to parent
+ )
+
+ return result
\ No newline at end of file
diff --git a/shader_noise_ksampler.py b/shader_noise_ksampler.py
new file mode 100644
index 0000000..ca29a64
--- /dev/null
+++ b/shader_noise_ksampler.py
@@ -0,0 +1,2185 @@
+import torch
+import math
+import comfy.sample
+import contextlib
+from nodes import common_ksampler
+from .shader_params_reader import get_shader_params, generate_noise_tensor
+# Add imports for custom sigma handling
+import comfy.samplers
+import comfy.model_sampling
+import comfy.model_base # For ModelType enum
+import comfy.latent_formats # For default latent format
+
+# Stub visualizer class as replacement for removed visualizer
+class StubVisualizer:
+ """A stub visualizer that implements the same interface but does nothing. Temporary replacement for removed visualizer."""
+ def __init__(self):
+ self.enabled = False
+
+ def enable(self, seed=None, shader_type=None, additional_metadata=None):
+ """Stub for enable method."""
+ pass
+
+ def disable(self):
+ """Stub for disable method."""
+ pass
+
+ def save_latent_visualization(self, tensor, label, stage_info=None, is_sample=False):
+ """Stub for save_latent_visualization method."""
+ pass
+
+ def save_denoising_step(self, tensor, stage_info, current_step, total_steps):
+ """Stub for save_denoising_step method."""
+ pass
+
+ def capture_shader_process(self, phase, stage_idx, stage_type, stage_data, base_noise, shader_noise, blended_noise, result):
+ """Stub for capture_shader_process method."""
+ pass
+
+ def capture_final_result(self, tensor, metadata=None):
+ """Stub for capture_final_result method."""
+ pass
+
+ def get_ui_image_paths(self):
+ """Stub for get_ui_image_paths method."""
+ return {
+ "base_noise": None,
+ "shader_noise": None,
+ "blended_noise": None,
+ "stage_results": None,
+ "final_result": None,
+ "grids": []
+ }
+
+# Also need a stub for the debugger
+class StubDebugger:
+ """A stub debugger that implements the same interface but does nothing."""
+ def __init__(self):
+ self.enabled = False
+ self.debug_level = 0
+
+ def reset(self):
+ """Stub for reset method."""
+ pass
+
+ def time_operation(self, name):
+ """Stub for time_operation method."""
+ return contextlib.nullcontext()
+
+ def analyze_tensor(self, tensor, name):
+ """Stub for analyze_tensor method."""
+ pass
+
+ def log_parameters(self, params):
+ """Stub for log_parameters method."""
+ pass
+
+ def log_stage_start(self, stage_type, stage_idx, params):
+ """Stub for log_stage_start method."""
+ pass
+
+ def log_stage_end(self, stage_type, stage_idx):
+ """Stub for log_stage_end method."""
+ pass
+
+ def log_blend_operation(self, base, shader, result, mode, strength):
+ """Stub for log_blend_operation method."""
+ pass
+
+# Functions to get the stub instances
+def get_visualizer():
+ """Return the stub visualizer instance."""
+ return StubVisualizer()
+
+def get_debugger():
+ """Return the stub debugger instance."""
+ return StubDebugger()
+
+def set_debug_level(level):
+ """Return the stub debugger with the specified level."""
+ debugger = get_debugger()
+ debugger.debug_level = level
+ debugger.enabled = level > 0
+ return debugger
+
+class CustomSigmaProvider:
+ """
+ A helper class to provide sigma values from a custom tensor,
+ mimicking the interface of comfy.model_sampling classes.
+ """
+ def __init__(self, sigmas_tensor):
+ # Ensure sigmas are sorted descending
+ if sigmas_tensor.numel() > 1: # Avoid error for single-element tensors
+ if torch.all(sigmas_tensor[1:] <= sigmas_tensor[:-1]): # Allow non-strict inequality
+ self.sigmas = sigmas_tensor.float()
+ elif torch.all(sigmas_tensor[1:] >= sigmas_tensor[:-1]): # Allow non-strict inequality
+ self.sigmas = torch.flip(sigmas_tensor, (0,)).float()
+ else:
+ # If not sorted or has duplicates, sort descending
+ self.sigmas = torch.sort(sigmas_tensor, descending=True)[0].float()
+ else:
+ self.sigmas = sigmas_tensor.float() # Handle single value tensor
+
+ self.log_sigmas = self.sigmas.log()
+ self.num_timesteps = len(self.sigmas)
+
+ @property
+ def sigma_min(self):
+ return self.sigmas[-1]
+
+ @property
+ def sigma_max(self):
+ return self.sigmas[0]
+
+ def timestep(self, sigma):
+ log_sigma = sigma.log()
+ log_sigmas_dev = self.log_sigmas.to(log_sigma.device)
+ dists = log_sigma - log_sigmas_dev[:, None]
+ indices = torch.sum(dists < 0, dim=0) # Find last index where log_sigma >= log_sigmas_dev[index]
+ # Handle edge case where sigma is larger than sigma_max
+ indices[log_sigma > log_sigmas_dev[0]] = 0
+ indices = torch.clamp(indices, 0, self.num_timesteps - 1)
+ return indices.view(sigma.shape).to(sigma.device)
+
+ def sigma(self, timestep):
+ sigmas_dev = self.sigmas.to(timestep.device)
+ log_sigmas_dev = self.log_sigmas.to(timestep.device)
+
+ t = torch.clamp(timestep.float(), min=0, max=(self.num_timesteps - 1))
+ # Use integer indices directly if timestep is integer
+ if torch.all(t == t.long()):
+ return sigmas_dev[t.long()].to(timestep.device)
+ else:
+ # Interpolate for float timesteps
+ low_idx = t.floor().long()
+ high_idx = t.ceil().long()
+ # Clamp indices to be within bounds [0, num_timesteps - 1]
+ low_idx = torch.clamp(low_idx, 0, self.num_timesteps - 1)
+ high_idx = torch.clamp(high_idx, 0, self.num_timesteps - 1)
+ w = t.frac()
+ # Perform interpolation using sigmas on the correct device
+ log_sigma_val = (1 - w) * log_sigmas_dev[low_idx] + w * log_sigmas_dev[high_idx]
+ return log_sigma_val.exp().to(timestep.device)
+
+class CustomSigmaModelWrapper:
+ """Wraps a model to override its model_sampling attribute."""
+ # Add device argument to __init__
+ def __init__(self, model, sigmas_tensor, device):
+ self.original_model = model
+ # Use the explicitly passed device
+ self.model_sampling = CustomSigmaProvider(sigmas_tensor.to(device))
+ # Copy essential attributes
+ self.model_type = getattr(model, 'model_type', comfy.model_base.ModelType.EPS)
+ self.latent_format = getattr(model, 'latent_format', comfy.latent_formats.SD15())
+ # Store the explicitly passed device
+ self.device = device
+
+ def __getattr__(self, name):
+ if name in ['original_model', 'model_sampling', 'model_type', 'latent_format', 'device']:
+ return object.__getattribute__(self, name)
+ try:
+ return getattr(self.original_model, name)
+ except AttributeError:
+ raise AttributeError(f"'{type(self).__name__}' object (wrapping '{type(self.original_model).__name__}') has no attribute '{name}'")
+
+ def apply_model(self, *args, **kwargs):
+ if hasattr(self.original_model, 'apply_model') and callable(self.original_model.apply_model):
+ return self.original_model.apply_model(*args, **kwargs)
+ else:
+ raise NotImplementedError(f"The wrapped model '{type(self.original_model).__name__}' does not have an 'apply_model' method.")
+
+# Define a registry for shader generators
+SHADER_GENERATORS = {}
+# Define a registry for shader generators
+SHADER_GENERATORS = {}
+
+# Function to get the appropriate generator
+def get_shader_generator(shader_type):
+ """Get the appropriate shader generator function based on shader type"""
+ generator = SHADER_GENERATORS.get(shader_type)
+ if generator:
+ return generator
+ else:
+ return generate_noise_tensor
+
+# Function to register shader generators
+def register_shader_generator(shader_type, generator_function):
+ """Register a shader generator function for a specific shader type"""
+ SHADER_GENERATORS[shader_type] = generator_function
+ return generator_function
+
+# Create a custom sampling callback class
+class DenoisingStepCallback:
+ def __init__(self, visualizer=None, stage_info=None, frequency="25% intervals"):
+ self.visualizer = visualizer
+ self.stage_info = stage_info or {}
+ self.frequency = frequency
+ self.enable_logging = visualizer is not None and visualizer.enabled
+ self.last_saved_step = -1
+ # For animation compatibility
+ self.total_steps_value = None
+ self.step_counter = 0
+
+ def __call__(self, step, x0, x, total_steps):
+ if not self.enable_logging:
+ return False
+
+ try:
+ # We'll use a step counter instead of the potentially problematic step input
+ current_step = self.step_counter
+
+ # On first call, try to extract total_steps safely
+ if self.total_steps_value is None:
+ # Try to safely get a scalar value
+ if isinstance(total_steps, int):
+ self.total_steps_value = total_steps
+ elif hasattr(total_steps, 'item') and hasattr(total_steps, 'numel') and total_steps.numel() == 1:
+ self.total_steps_value = total_steps.item()
+ else:
+ # Default value if we can't determine
+ self.total_steps_value = 20
+ print(f"โ ๏ธ Using default value of {self.total_steps_value} steps for visualization")
+
+ # If we're at the beginning of a new sampling iteration, reset our counter
+ if isinstance(step, int) and step == 0:
+ self.step_counter = 0
+
+ # Determine if we should save this step based on the selected frequency
+ should_save = False
+
+ # Always save first and last step
+ if current_step == 0 or current_step >= self.total_steps_value - 1:
+ should_save = True
+ # Apply frequency filter
+ elif self.frequency == "Every step":
+ should_save = True
+ elif self.frequency == "25% intervals":
+ step_percent = (current_step / self.total_steps_value) * 100
+ interval_size = (1 / self.total_steps_value) * 100
+ should_save = (step_percent % 25 < interval_size)
+ elif self.frequency == "10% intervals":
+ step_percent = (current_step / self.total_steps_value) * 100
+ interval_size = (1 / self.total_steps_value) * 100
+ should_save = (step_percent % 10 < interval_size)
+ elif self.frequency == "4 steps":
+ should_save = (current_step % max(1, self.total_steps_value // 4) == 0)
+ elif self.frequency == "2 steps":
+ should_save = (current_step % max(1, self.total_steps_value // 2) == 0)
+
+ # Check to avoid duplicate steps
+ if should_save and current_step != self.last_saved_step:
+ try:
+ # Safely get a copy of the tensor
+ if hasattr(x, 'clone'):
+ x_copy = x.clone()
+ else:
+ x_copy = x
+
+ # Capture intermediate denoising state
+ self.visualizer.save_denoising_step(
+ x_copy,
+ self.stage_info,
+ current_step,
+ self.total_steps_value
+ )
+ self.last_saved_step = current_step
+ except Exception as e:
+ print(f"โ ๏ธ Error saving denoising step: {e}")
+
+ # Increment our step counter
+ self.step_counter += 1
+
+ except Exception as e:
+ print(f"โ ๏ธ Error in denoising callback: {e}")
+
+ # Always return False to continue processing
+ return False
+
+# Modify the shader_ksampler function to use our callback
+def shader_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, noise_tensor, denoise=1.0, stage_info=None, denoise_visualization_frequency="25% intervals"):
+ """
+ Custom modification of common_ksampler that allows us to inject our shader noise
+ """
+ # Get the debugger
+ debugger = get_debugger()
+
+ # Get the visualizer
+ visualizer = get_visualizer()
+
+ # Create stage info if not provided
+ if stage_info is None:
+ stage_info = {
+ "stage_type": "standard",
+ "stage_idx": 0
+ }
+
+ # Get the device from the latent image
+ device = latent_image["samples"].device
+
+ # Set seed for consistent sampling
+ torch.manual_seed(seed)
+
+ # Extract latent samples
+ latent_samples = latent_image["samples"]
+ is_video = len(latent_samples.shape) == 5
+ channel_dim = 2 if is_video else 1
+ spatial_dims_start = 3 if is_video else 2
+ num_spatial_dims = len(latent_samples.shape) - spatial_dims_start
+
+ # Log initial tensors if debugging enabled
+ if debugger.enabled:
+ with debugger.time_operation("shader_ksampler_setup"):
+ debugger.analyze_tensor(latent_samples, "initial_latent")
+ debugger.analyze_tensor(noise_tensor, "initial_noise")
+
+ # Save initial latent and noise if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ latent_samples.clone(),
+ "Initial Latent",
+ stage_info=stage_info,
+ is_sample=True
+ )
+ visualizer.save_latent_visualization(
+ noise_tensor.clone(),
+ "Initial Noise",
+ stage_info=stage_info
+ )
+
+ # Make sure noise is on the right device
+ noise = noise_tensor.to(device)
+
+ # Check if shapes match
+ if noise.shape != latent_samples.shape:
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"โ ๏ธ Shape mismatch detected: Noise {noise.shape} vs Latent {latent_samples.shape}")
+
+ # Handle channel mismatch
+ if noise.shape[channel_dim] != latent_samples.shape[channel_dim]:
+ # Create a new noise tensor with the correct number of channels
+ new_noise_shape = list(latent_samples.shape)
+ new_noise = torch.randn(new_noise_shape, device=device, dtype=noise.dtype)
+
+ # Copy over the values from the original noise tensor for channels that exist
+ min_channels = min(noise.shape[channel_dim], latent_samples.shape[channel_dim])
+
+ # Use slicing based on dimension type
+ if is_video:
+ new_noise[:, :, :min_channels, ...] = noise[:, :, :min_channels, ...]
+ else:
+ new_noise[:, :min_channels, ...] = noise[:, :min_channels, ...]
+
+ noise = new_noise
+
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"โ Resized noise channels to match latent dimensions: {noise.shape}")
+ if debugger.debug_level >= 2:
+ debugger.analyze_tensor(noise, "resized_noise_channels")
+ # Save resized noise if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ noise.clone(),
+ "Resized Noise (Channels)",
+ stage_info=stage_info
+ )
+
+ # Handle spatial dimension mismatch (only if channels already match or were just fixed)
+ else:
+ # Check if any spatial dimension mismatches
+ spatial_mismatch = False
+ for i in range(num_spatial_dims):
+ dim_index = spatial_dims_start + i
+ if noise.shape[dim_index] != latent_samples.shape[dim_index]:
+ spatial_mismatch = True
+ break
+
+ if spatial_mismatch:
+ target_spatial_size = latent_samples.shape[spatial_dims_start:]
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"Attempting to resize spatial dimensions {noise.shape[spatial_dims_start:]} -> {target_spatial_size}")
+
+ # Interpolate spatial dimensions
+ try:
+ # Determine interpolation mode based on dimensions
+ if num_spatial_dims == 1:
+ mode = 'linear'
+ elif num_spatial_dims == 2:
+ mode = 'bilinear'
+ elif num_spatial_dims == 3:
+ mode = 'trilinear'
+ else:
+ # Fallback for unexpected dimensions
+ mode = 'nearest'
+ print(f"โ ๏ธ Unexpected number of spatial dimensions ({num_spatial_dims}). Using 'nearest' interpolation.")
+
+ noise = torch.nn.functional.interpolate(
+ noise,
+ size=target_spatial_size,
+ mode=mode,
+ align_corners=False if mode in ['linear', 'bilinear', 'trilinear'] else None
+ )
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"โ Resized noise spatial dimensions to match latent: {noise.shape}")
+ except Exception as e:
+ print(f"โ Error during spatial interpolation: {e}. Skipping resize.")
+ # Optionally fallback: noise = torch.zeros_like(latent_samples) ?
+
+ # Save resized noise if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ noise.clone(),
+ "Resized Noise (Spatial)",
+ stage_info=stage_info
+ )
+
+ # Get batch index if available
+ batch_inds = latent_image.get("batch_index", None)
+
+ # Reset random seed before sampling to ensure deterministic behavior
+ # This is critical for temporal coherence
+ torch.manual_seed(seed)
+
+ # Log before sampling
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f"๐ Starting sampling with {sampler_name}, {steps} steps, CFG {cfg:.2f}, denoise {denoise:.2f}")
+
+ # Create our denoising step callback with frequency parameter
+ callback = DenoisingStepCallback(visualizer, stage_info, denoise_visualization_frequency)
+
+ # Use comfy.sample.sample directly to apply our noise
+ with debugger.time_operation("sampling_process") if debugger.enabled else contextlib.nullcontext():
+ output_samples = comfy.sample.sample(
+ model=model,
+ noise=noise,
+ steps=steps,
+ cfg=cfg,
+ sampler_name=sampler_name,
+ scheduler=scheduler,
+ positive=positive,
+ negative=negative,
+ latent_image=latent_samples,
+ denoise=denoise,
+ disable_noise=True, # We've already generated the noise
+ start_step=0,
+ last_step=steps,
+ force_full_denoise=True,
+ seed=seed, # Pass seed value explicitly
+ callback=callback.__call__ # Add our callback to capture intermediate steps
+ )
+
+ # Log after sampling
+ if debugger.enabled:
+ debugger.analyze_tensor(output_samples, "output_samples")
+
+ # Save output samples if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ output_samples.clone(),
+ "Sampled Output",
+ stage_info=stage_info,
+ is_sample=True
+ )
+
+ # Restore original random state
+ torch.manual_seed(torch.seed())
+
+
+ # Format output while preserving any metadata that might be useful for temporal coherence
+ out = latent_image.copy()
+ out["samples"] = output_samples
+
+ return (out, )
+
+class ShaderNoiseKSampler:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "model": ("MODEL", {"tooltip": "The AI model used for image generation"}),
+ "seed": ("INT", {"default": 8888, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Random seed for generation. Same seed with same parameters will generate the same image. Use fixed seed for shader noise exploration."}),
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000, "tooltip": "Number of sampling steps. Higher values can produce better results but take longer"}),
+ "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.1, "tooltip": "Classifier-free guidance scale. Higher values follow the prompt more closely"}),
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler_ancestral", "tooltip": "Algorithm used for the sampling process"}),
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "beta", "tooltip": "Scheduler used to determine noise level at each step"}),
+ "positive": ("CONDITIONING", {"tooltip": "Positive conditioning/prompts that guide what to include in the image"}),
+ "negative": ("CONDITIONING", {"tooltip": "Negative conditioning/prompts that guide what to exclude from the image"}),
+ "latent_image": ("LATENT", {"tooltip": "Input latent image to be processed"}),
+ "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Denoising strength. Lower values preserve more of the original image"}),
+ "sequential_stages": ("INT", {"default": 1, "min": 0, "max": 10, "step": 1, "tooltip": "Number of sequential shader stages to apply before injection stages"}),
+ "injection_stages": ("INT", {"default": 0, "min": 0, "max": 10, "step": 1, "tooltip": "Number of injection shader stages to apply after sequential stages"}),
+ "shader_strength": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Strength of the shader noise influence for both stage types - set to 0.0 to disable and use only the base noise"}),
+ "blend_mode": (["normal", "add", "multiply", "screen", "overlay", "soft_light", "hard_light", "difference"], {"default": "multiply", "tooltip": "Method used to blend the shader noise with the base noise"}),
+ "noise_transform": (["none", "reverse", "inverse", "absolute", "square", "sqrt", "log", "sin", "cos"], {"default": "none", "tooltip": "Apply mathematical transformations to the noise for creative effects"}),
+ "use_temporal_coherence": ("BOOLEAN", {"default": False, "tooltip": "Ensures consistent noise patterns. For sequences like video frames, it helps maintain frame-to-frame consistency (e.g., using the same base seed and 4D noise). For single image generations, it ensures the base noise is derived consistently from the main seed."}),
+
+ },
+ "optional": { # Add the optional input here
+ "custom_sigmas": ("SIGMAS", {"tooltip": "Optional custom sigma schedule to override the model's default schedule"}),
+ },
+ "hidden": {
+ "show_default_preview": ("BOOLEAN", {"default": False}),
+ "show_custom_preview": ("BOOLEAN", {"default": False, "tooltip": "Whether to show the custom positioned preview outside the node"}),
+ "fast_high_channel_noise": ("BOOLEAN", {"default": False, "tooltip": "Use a faster, simplified noise generation method for models with many channels (>16), like LTXV."}),
+ "sequential_distribution": (["uniform", "linear_decrease", "linear_increase", "gaussian", "first_stronger", "last_stronger"], {"default": "linear_decrease", "tooltip": "How shader strength is distributed across sequential stages"}),
+ "injection_distribution": (["uniform", "linear_decrease", "linear_increase", "gaussian", "first_stronger", "last_stronger"], {"default": "linear_decrease", "tooltip": "How shader strength is distributed across injection stages"}),
+ "debug_level": (["0-Off", "1-Basic", "2-Detailed", "3-Verbose"], {"default": "0-Off", "tooltip": "Enable debugging at specified level to understand what's happening during shader generation and sampling"}),
+ "save_visualizations": ("BOOLEAN", {"default": False, "tooltip": "Save visualizations of the process stages to disk regardless of debug level"}),
+ "denoise_visualization_frequency": (["Every step", "25% intervals", "10% intervals", "4 steps", "2 steps"], {"default": "Every step", "tooltip": "How often to save images during the denoising process. Higher frequency means more images but slower generation."}),
+ "target_attribute_changes": ("STRING", {"forceInput": True, "tooltip": "Connect output from ParameterResponseMapperNode here"}),
+ }
+ }
+
+ RETURN_TYPES = ("LATENT",)
+ FUNCTION = "sample"
+ CATEGORY = "sampling"
+
+ @classmethod
+ def IS_CHANGED(s, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
+ denoise, sequential_stages, injection_stages, shader_strength, blend_mode, noise_transform="none",
+ sequential_distribution="linear_decrease", injection_distribution="linear_decrease", use_temporal_coherence=False, debug_level="0-Off",
+ save_visualizations=False, denoise_visualization_frequency="Every step", custom_sigmas=None, # Add custom_sigmas here
+ target_attribute_changes="", # Add target_attribute_changes here
+ fast_high_channel_noise=False, # Add fast_high_channel_noise here
+ show_custom_preview=False, show_default_preview=False, shader_params_override=None):
+ # This will force a re-execution of the node when params change
+ # Include custom_sigmas in the check. Use a simple hash or just the object itself.
+ # Using the object itself works for comparison; hashing tensor content is more robust if needed
+ # but usually comparing the tensor object ID or a simple representation is sufficient for IS_CHANGED.
+ # We return a tuple of things that, if changed, should trigger a re-run.
+ # Note: Comparing the tensor directly might not always work if a new identical tensor is passed.
+ # A more robust way might involve hashing, but let's start simple.
+ return (seed, steps, cfg, sampler_name, scheduler, denoise, sequential_stages,
+ injection_stages, shader_strength, blend_mode, noise_transform,
+ sequential_distribution, injection_distribution, use_temporal_coherence,
+ debug_level, save_visualizations, denoise_visualization_frequency,
+ custom_sigmas, target_attribute_changes, fast_high_channel_noise) # Add custom_sigmas, target_attribute_changes and fast_high_channel_noise to the tuple
+
+ @classmethod
+ def CONTEXT_MENUS(s):
+ return {
+ "Show Custom Preview": lambda self, **kwargs: {"show_custom_preview": False},
+ "Hide Custom Preview": lambda self, **kwargs: {"show_custom_preview": False}
+ }
+
+ def has_preview(self, show_default_preview):
+ # Use the parameter to decide whether to allow the default preview
+ return show_default_preview
+
+ def _calculate_stage_strengths(self, base_strength, num_stages, distribution):
+ """Calculate strength for each stage based on distribution type"""
+ strengths = []
+
+ if distribution == "uniform":
+ strengths = [base_strength] * num_stages
+ elif distribution == "linear_decrease":
+ min_factor = 0.25 # Ensure a minimum shader strength in each stage
+ for i in range(num_stages):
+ factor = 1.0 - (i / (num_stages - 1 if num_stages > 1 else 1))
+ factor = max(factor, min_factor)
+ strengths.append(base_strength * factor)
+ elif distribution == "linear_increase":
+ for i in range(num_stages):
+ factor = i / (num_stages - 1 if num_stages > 1 else 1)
+ strengths.append(base_strength * factor)
+ elif distribution == "gaussian":
+ # Create a bell curve with peak in the middle
+ mid_point = (num_stages - 1) / 2
+ for i in range(num_stages):
+ # Standard deviation as 1/3 of the range
+ std_dev = num_stages / 3
+ factor = math.exp(-((i - mid_point) ** 2) / (2 * std_dev ** 2))
+ strengths.append(base_strength * factor)
+ elif distribution == "first_stronger":
+ # First stage strongest, rapid falloff
+ for i in range(num_stages):
+ factor = math.exp(-i) # Exponential decay
+ strengths.append(base_strength * factor)
+ elif distribution == "last_stronger":
+ # Last stage strongest, exponential increase
+ for i in range(num_stages):
+ factor = math.exp(i - num_stages + 1) # Exponential increase
+ strengths.append(base_strength * factor)
+ else:
+ # Default to uniform if unknown distribution
+ strengths = [base_strength] * num_stages
+
+ return strengths
+
+ def _calculate_step_ranges(self, total_steps, num_stages):
+ """Calculate step ranges for sequential stages"""
+ if num_stages <= 0:
+ return []
+
+ step_ranges = []
+ step_size = total_steps / num_stages
+
+ for i in range(num_stages):
+ start_step = int(i * step_size)
+ end_step = int((i + 1) * step_size) if i < num_stages - 1 else total_steps
+ step_ranges.append((start_step, end_step))
+
+ return step_ranges
+
+ def _calculate_step_points(self, total_steps, num_stages):
+ """Calculate at which steps to apply different shader noises for injection stages"""
+ if num_stages <= 1:
+ return [0]
+
+ step_points = []
+ for i in range(num_stages):
+ step_point = int(i * total_steps / (num_stages - 1)) if num_stages > 1 else 0
+ step_points.append(min(step_point, total_steps - 1)) # Ensure we don't exceed total steps
+
+ return step_points
+
+ def _apply_noise_transform(self, noise, transform):
+ """Apply mathematical transformations to noise"""
+ if transform == "none":
+ return noise
+ elif transform == "reverse":
+ return -noise
+ elif transform == "inverse":
+ # Add small epsilon to avoid division by zero
+ return 1.0 / (noise + 1e-8)
+ elif transform == "absolute":
+ return torch.abs(noise)
+ elif transform == "square":
+ return noise ** 2
+ elif transform == "sqrt":
+ return torch.sqrt(torch.abs(noise))
+ elif transform == "log":
+ return torch.log(torch.abs(noise) + 1.0)
+ elif transform == "sin":
+ return torch.sin(noise * math.pi)
+ elif transform == "cos":
+ return torch.cos(noise * math.pi)
+ else:
+ return noise # Default to no transform
+
+ def get_model_channel_count(self, model):
+ """
+ Determine the number of channels in the latent space for a given model.
+ This is important for matching the correct noise shape to the model's expectations.
+
+ Args:
+ model: The model to analyze
+
+ Returns:
+ int: Number of channels (4 for SD1.5/2.x, 16 for WAN2.1, 32 for SD3)
+ """
+ # Get debugger
+ debugger = get_debugger()
+
+ # Check for different model types
+ model_name = getattr(model, 'model_name', "").lower() if isinstance(getattr(model, 'model_name', ""), str) else ""
+ # Safely convert model_type to string before using upper()
+ model_type_attr = getattr(model, 'model_type', "")
+ model_type = str(model_type_attr).upper() if model_type_attr is not None else ""
+ model_class = model.__class__.__name__
+
+ if debugger.enabled:
+ print(f"๐ Model detection - name: '{model_name}', type: '{model_type}', class: '{model_class}'")
+ # Check if model is a ModelPatcher or similar wrapper
+ if hasattr(model, 'model'):
+ inner_model = model.model
+ inner_name = getattr(inner_model, 'model_name', "").lower() if isinstance(getattr(inner_model, 'model_name', ""), str) else ""
+ # Safely convert inner_model_type to string before using upper()
+ inner_type_attr = getattr(inner_model, 'model_type', "")
+ inner_type = str(inner_type_attr).upper() if inner_type_attr is not None else ""
+ inner_class = inner_model.__class__.__name__
+ print(f"๐ Inner model - name: '{inner_name}', type: '{inner_type}', class: '{inner_class}'")
+
+ # Add check for HiDream / FLOW type specifically for 16 channels
+ if inner_class == "HiDream" or inner_type == "FLOW":
+ print(f"โ Detected HiDream/Flow in inner model - using 16 channels")
+ return 16
+ # Check for ACEStep/ACE for 8 channels
+ elif inner_class == "ACEStep" or inner_class == "ACE":
+ print(f"โ Detected {inner_class} in inner model - using 8 channels")
+ return 8
+ # Check for corrected WAN21/Warp (now 16 channels)
+ elif "wan" in inner_name or "warp" in inner_name or inner_class == "WAN21":
+ print(f"โ Detected WAN/Warp in inner model - using 16 channels") # Corrected from 6
+ return 16
+ # Check for specific high-channel video/3D models
+ elif inner_class == "Mochi":
+ print(f"โ Detected Mochi in inner model - using 12 channels")
+ return 12
+ elif inner_class == "LTXV":
+ print(f"โ Detected LTXV in inner model - using 128 channels")
+ return 128
+ elif inner_class == "CosmosVideo": # Add specific check for CosmosVideo
+ print(f"โ Detected CosmosVideo in inner model - using 16 channels")
+ return 16
+ elif inner_class == "Cosmos1CV8x8x8":
+ print(f"โ Detected Cosmos1CV8x8x8 in inner model - using 16 channels")
+ return 16
+ # Explicit check for HunyuanVideo variants which use more channels
+ elif inner_class == "HunyuanVideo":
+ print(f"โ Detected HunyuanVideo class in inner model - using 16 channels")
+ return 16
+ # Check for Stable Cascade Prior
+ elif "stable_cascade_prior" in inner_name or "stablecascade_prior" in inner_name:
+ print(f"โ Detected Stable Cascade Prior in inner model - using 16 channels")
+ return 16
+
+ # Print model's unet structure if available for debugging
+ if hasattr(model, 'diffusion_model') and debugger.debug_level >= 2:
+ if hasattr(model.diffusion_model, 'in_channels'):
+ print(f"๐ Model's diffusion_model.in_channels: {model.diffusion_model.in_channels}")
+ if hasattr(model.diffusion_model, 'input_blocks') and hasattr(model.diffusion_model.input_blocks[0], 'in_channels'):
+ print(f"๐ Model's input_blocks[0].in_channels: {model.diffusion_model.input_blocks[0].in_channels}")
+
+ # Check latent shape by looking at both the model and any wrapped models
+ if hasattr(model, 'latent_format') and hasattr(model.latent_format, 'latent_channels'):
+ channels = model.latent_format.latent_channels
+ if debugger.enabled:
+ print(f"โ Found latent_format.latent_channels: {channels}")
+ return channels
+
+ # Examine diffusion model's channel structure
+ if hasattr(model, 'diffusion_model'):
+ # Look at the first input block's in_channels which often reveals the model's channel count
+ if hasattr(model.diffusion_model, 'input_blocks') and len(model.diffusion_model.input_blocks) > 0:
+ if hasattr(model.diffusion_model.input_blocks[0], 'in_channels'):
+ channels = model.diffusion_model.input_blocks[0].in_channels
+ # In_channels is often double the latent space channels
+ if channels in [8, 12, 16]:
+ actual_channels = channels // 2
+ if debugger.enabled:
+ print(f"โ Detected {actual_channels} channels from model's input_blocks[0].in_channels={channels}")
+ return actual_channels
+
+ # Check for patch_embedding
+ if hasattr(model.diffusion_model, 'patch_embedding'):
+ patch_shape = model.diffusion_model.patch_embedding.weight.shape
+ if len(patch_shape) >= 2:
+ channels = patch_shape[1]
+ if debugger.enabled:
+ print(f"โ Found patch_embedding with input channels: {channels}")
+ return channels
+
+ # Specific model family detection (check outer model too)
+ wan_indicators = ["wan", "warp", "pixel", "anime"] # Removed "flow" as it's handled above for inner HiDream/16ch
+ if any(ind in model_name for ind in wan_indicators) or model_class == "WAN21":
+ if debugger.enabled:
+ print(f"โ Detected WAN/Warp/Pixel/Anime model by name/type - using 16 channels") # Corrected from 6
+ return 16
+ elif "sd3" in model_name:
+ if debugger.enabled:
+ print(f"โ Detected SD3 model by name - using 16 channels") # Corrected from 8
+ return 16
+ elif "flux" in model_name:
+ if debugger.enabled:
+ print(f"โ Detected Flux model by name - using 16 channels")
+ return 16
+ elif "mochi" in model_name:
+ if debugger.enabled:
+ print(f"โ Detected Mochi model by name - using 12 channels")
+ return 12
+ elif "ltxv" in model_name:
+ if debugger.enabled:
+ print(f"โ Detected LTXV model by name - using 128 channels")
+ return 128
+ elif "cosmos" in model_name:
+ if debugger.enabled:
+ print(f"โ Detected Cosmos model by name - using 16 channels")
+ return 16
+ elif "stable_cascade_prior" in model_name or "stablecascade_prior" in model_name:
+ if debugger.enabled:
+ print(f"โ Detected Stable Cascade Prior model by name - using 16 channels")
+ return 16
+ elif "stable_cascade" in model_name: # Assuming non-prior Stable Cascade is 4 channels
+ if debugger.enabled:
+ print(f"โ Detected Stable Cascade (non-prior) model by name - using 4 channels")
+ return 4
+
+ # Explicit checks for model classes (Flux, HiDream)
+ model_class_name = model.__class__.__name__.lower()
+ inner_model_class_name = getattr(model.model, '__class__', None).__name__.lower() if hasattr(model, 'model') else None
+
+ if 'flux' == model_class_name or 'flux' == inner_model_class_name:
+ if debugger.enabled:
+ print(f"โ Detected Flux model by class ({model_class_name}/{inner_model_class_name}) - using 16 channels")
+ return 16
+ elif 'hidream' == model_class_name or 'hidream' == inner_model_class_name:
+ # This might be redundant if name check catches it, but good as fallback
+ if debugger.enabled:
+ print(f"โ Detected HiDream model by class ({model_class_name}/{inner_model_class_name}) - using 16 channels")
+ return 16
+
+ # Look at model path if available (CheckpointLoaderSimple might set this)
+ model_path = getattr(model, 'model_path', "").lower()
+ # Update path check for WAN indicators (now 16 channels)
+ if model_path and any(ind in model_path for ind in wan_indicators):
+ if debugger.enabled:
+ print(f"โ Detected WAN/Warp/Pixel/Anime model from model_path - using 16 channels") # Corrected from 6
+ return 16
+
+ # Default for most models (SD1.5, SD2.x, SDXL, Stable Cascade B, SD_X4 etc)
+ if debugger.enabled:
+ print(f"๐ Using default channel count: 4")
+ return 4
+
+ def _generate_shader_noise(self, latent_samples, target_noise_shape, shader_params, shader_type, seed, device="cuda", model=None, model_name=None, frame_count=1, frame_dim_idx=-1):
+ """
+ Generate noise using the specified shader
+
+ Args:
+ latent_samples: The input latent samples (for context/shape)
+ target_noise_shape: Expected shape of the final noise tensor
+ shader_params: Parameters for shader generation
+ shader_type: Type of shader generator to use
+ seed: Random seed
+ device: Device to create tensor on
+ model: The model object (for channel detection)
+ model_name: Optional model name for customized generation
+ frame_count: Number of frames to generate (deduced from shape)
+ frame_dim_idx: Index of the frame dimension (-1 if not video)
+
+ Returns:
+ torch.Tensor: Generated noise tensor with shape matching target_noise_shape
+ """
+ # Get debugger
+ debugger = get_debugger()
+
+ # Set random seed for reproducibility
+ torch.manual_seed(seed)
+
+ # Extract shape information from the target_noise_shape
+ is_video = len(target_noise_shape) == 5
+ batch_size = target_noise_shape[0]
+ height = target_noise_shape[-2]
+ width = target_noise_shape[-1]
+
+ # Get channels from target_noise_shape (use correct index)
+ if is_video:
+ # Determine channel index based on target shape format
+ # If dim 1 matches model channels, likely [B, C, F, H, W]
+ # If dim 2 matches model channels, likely [B, F, C, H, W]
+ # Use the passed frame_dim_idx to deduce channel_dim_idx
+ channel_dim_idx = 3 - frame_dim_idx # If frame=1, channel=2; if frame=2, channel=1
+ channels = target_noise_shape[channel_dim_idx]
+ else:
+ # 4D: [B, C, H, W]
+ channels = target_noise_shape[1]
+ channel_dim_idx = 1
+
+ # Add model information to shader params to aid in detection
+ shader_params = shader_params.copy()
+
+ # If a model is provided, extract all possible identifying information
+ if model is not None:
+ # Get and store model name
+ if model_name is None:
+ model_name = getattr(model, 'model_name', None)
+ if model_name is None and hasattr(model, 'model') and hasattr(model.model, 'model_name'):
+ model_name = model.model.model_name
+
+ # Always store model_name in shader_params if available
+ if model_name:
+ shader_params["model_name"] = model_name
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f"๐ Added model_name='{model_name}' to shader_params")
+
+ # Add model class name which can be useful for detection
+ shader_params["model_class"] = model.__class__.__name__
+
+ # Add inner model class name if it exists
+ if hasattr(model, 'model'):
+ shader_params["inner_model_class"] = model.model.__class__.__name__
+ inner_model_name = getattr(model.model, 'model_name', None)
+ if inner_model_name:
+ shader_params["inner_model_name"] = inner_model_name
+
+ # Get target channels from model (should match 'channels' variable now)
+ model_target_channels = self.get_model_channel_count(model)
+ if model_target_channels != channels:
+ print(f"โ ๏ธ Discrepancy: Model expects {model_target_channels} channels, but target noise shape indicates {channels}.")
+ # We will proceed with the 'channels' derived from target_noise_shape
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f"๐จ Generating {shader_type} shader noise for {'video' if is_video else 'image'}")
+ print(f" Target Shape: {target_noise_shape}, Device: {device}, Seed: {seed}")
+ if model_name:
+ print(f" Model: {model_name}")
+ print(f" Target channels: {channels}")
+
+ # Add channel count to shader parameters
+ shader_params["target_channels"] = channels
+
+ # Log the final shader params for debugging
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f"๐ Final shader_params for noise generation: {shader_params}")
+
+ # For video latents
+ if is_video:
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f" Video dimensions: Target={target_noise_shape}, Frames={frame_count}")
+
+ # Get the shader generator function
+ generator_func = get_shader_generator(shader_type)
+ # Add print for shader_type in _generate_shader_noise
+ print(f"_generate_shader_noise: Using shader_type: {shader_type}")
+
+ # Generate noise frame by frame
+ noise_frames = []
+ # Use the correctly determined frame_count
+ for frame_idx in range(frame_count):
+ # Update time parameter for temporal variation
+ frame_params = shader_params.copy()
+ # Ensure time increments correctly even if frame_count < 10
+ frame_time_increment = 0.1 if frame_count <= 1 else (1.0 / (frame_count - 1)) * frame_idx
+ frame_params["time"] = shader_params.get("time", 0.0) + frame_time_increment
+
+ if debugger.enabled and debugger.debug_level >= 3:
+ print(f" Generating frame {frame_idx} with time={frame_params['time']:.2f}")
+
+ # Prepare arguments based on what the function accepts
+ # Note: The generator usually expects [B, C, H, W] per frame
+ generator_args = {
+ "shader_params": frame_params,
+ "height": height,
+ "width": width,
+ "batch_size": batch_size,
+ "device": device,
+ "seed": seed + frame_idx if not shader_params.get("useTemporalCoherence", False) else seed,
+ "target_channels": channels # Always include target_channels
+ }
+
+ # Generate the noise for this frame using named parameters
+ # Expected output shape: [batch_size, channels, height, width]
+ frame_noise = generator_func(**generator_args)
+
+ # Validate frame_noise shape
+ expected_frame_shape = (batch_size, channels, height, width)
+ if frame_noise.shape != expected_frame_shape:
+ # print(f"โ ๏ธ Shader generated frame shape {frame_noise.shape} instead of expected {expected_frame_shape}")
+
+ # Attempt to correct the frame shape
+ try:
+ # If channel count is wrong, fix it
+ if frame_noise.shape[1] != channels:
+ # print(f" Correcting frame channel count from {frame_noise.shape[1]} to {channels}")
+ corrected_frame = torch.zeros(expected_frame_shape, device=device, dtype=frame_noise.dtype)
+ min_c = min(frame_noise.shape[1], channels)
+ corrected_frame[:, :min_c] = frame_noise[:, :min_c]
+ frame_noise = corrected_frame
+
+ # If spatial dimensions are wrong, resize
+ # This requires importing torch.nn.functional as F at the top of the file
+ if frame_noise.shape[2:] != expected_frame_shape[2:]:
+ # print(f" Resizing frame spatial dimensions from {frame_noise.shape[2:]} to {expected_frame_shape[2:]}")
+ import torch.nn.functional as F # Ensure F is available
+ frame_noise = F.interpolate(frame_noise, size=(height, width), mode='bilinear', align_corners=False)
+
+ # Final check
+ if frame_noise.shape != expected_frame_shape:
+ raise ValueError("Shape correction failed")
+
+ except Exception as e:
+ # print(f"โ Error correcting frame shape: {e}. Skipping frame.")
+ # Create a zero tensor as fallback to avoid crashing
+ frame_noise = torch.zeros(expected_frame_shape, device=device, dtype=latent_samples.dtype)
+
+ noise_frames.append(frame_noise)
+
+ # Stack frames along the correct dimension
+ if noise_frames:
+ noise = torch.stack(noise_frames, dim=frame_dim_idx)
+ else:
+ # Handle case where no frames were generated
+ print("โ No frames generated for video noise. Creating zeros.")
+ noise = torch.zeros(target_noise_shape, device=device, dtype=latent_samples.dtype)
+
+ # For image latents
+ else:
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f" Image dimensions: Target={target_noise_shape}")
+
+ # Get the shader generator function
+ generator_func = get_shader_generator(shader_type)
+ # Add print for shader_type in _generate_shader_noise
+ print(f"_generate_shader_noise: Using shader_type: {shader_type}")
+
+ # Prepare arguments based on what the function accepts
+ generator_args = {
+ "shader_params": shader_params,
+ "height": height,
+ "width": width,
+ "batch_size": batch_size,
+ "device": device,
+ "seed": seed,
+ "target_channels": channels # Always include target_channels
+ }
+
+ # Generate the noise with named parameters
+ noise = generator_func(**generator_args)
+
+ # Ensure image has correct final shape
+ if noise.shape != target_noise_shape:
+ print(f"โ ๏ธ Image shader generated shape {noise.shape} instead of expected {target_noise_shape}")
+ # Attempt correction (similar to frame correction)
+ try:
+ if noise.shape[1] != channels:
+ corrected_noise = torch.zeros(target_noise_shape, device=device, dtype=noise.dtype)
+ min_c = min(noise.shape[1], channels)
+ corrected_noise[:, :min_c] = noise[:, :min_c]
+ noise = corrected_noise
+ if noise.shape[2:] != target_noise_shape[2:]:
+ noise = F.interpolate(noise, size=(height, width), mode='bilinear', align_corners=False)
+ if noise.shape != target_noise_shape:
+ raise ValueError("Shape correction failed")
+ except Exception as e:
+ print(f"โ Error correcting image shape: {e}. Creating zeros.")
+ noise = torch.zeros(target_noise_shape, device=device, dtype=latent_samples.dtype)
+
+ # Normalize the noise tensor to have standard deviation of 1.0 and mean of 0.0
+ # This ensures consistent blending behavior regardless of the underlying distribution
+ if noise.numel() > 0: # Only attempt normalization if tensor has elements
+ # Calculate current mean and standard deviation
+ current_mean = noise.mean()
+ current_std = noise.std()
+
+ # Only normalize if standard deviation is not very close to zero
+ if current_std > 1e-6:
+ # Normalize: (x - mean) / std
+ noise = (noise - current_mean) / current_std
+
+ # Analyze and log the generated noise for debugging
+ if debugger.enabled:
+ debugger.analyze_tensor(noise, "shader_noise")
+
+ # Final shape check
+ if noise.shape != target_noise_shape:
+ print(f"โ FINAL SHAPE MISMATCH: Noise shape {noise.shape} != Target shape {target_noise_shape}")
+ # Attempt final resize as last resort
+ try:
+ noise = F.interpolate(noise, size=target_noise_shape[2:], mode='bilinear', align_corners=False)
+ # If channels are still wrong, adjust
+ if noise.shape[channel_dim_idx] != target_noise_shape[channel_dim_idx]:
+ final_noise = torch.zeros(target_noise_shape, device=device, dtype=noise.dtype)
+ min_c = min(noise.shape[channel_dim_idx], target_noise_shape[channel_dim_idx])
+ # Slicing needs to be careful based on dim order
+ if channel_dim_idx == 1: # [B, C, F, H, W]
+ final_noise[:,:min_c,...] = noise[:,:min_c,...]
+ elif channel_dim_idx == 2: # [B, F, C, H, W]
+ final_noise[:,:,:min_c,...] = noise[:,:,:min_c,...]
+ else: # 4D
+ final_noise[:,:min_c,...] = noise[:,:min_c,...]
+ noise = final_noise
+ except Exception as e:
+ print(f"โ Final resize attempt failed: {e}")
+ noise = torch.zeros(target_noise_shape, device=device, dtype=latent_samples.dtype)
+
+ return noise
+
+ def _blend_noises(self, base_noise, shader_noise, blend_mode, strength):
+ """
+ Blend base noise with shader noise using the specified blend mode and strength.
+ Handles channel dimension mismatches automatically.
+
+ Args:
+ base_noise: Base noise tensor
+ shader_noise: Shader noise tensor
+ blend_mode: Blending mode to apply
+ strength: Strength of the blend [0.0-1.0]
+
+ Returns:
+ torch.Tensor: Blended noise tensor
+ """
+ # Get the debugger
+ debugger = get_debugger()
+
+ # If strength is 0, return base noise unchanged
+ if strength <= 0.0:
+ return base_noise
+
+ # If strength is 1.0 and blend mode is "normal", return shader noise
+ if strength >= 1.0 and blend_mode == "normal":
+ # Ensure shader noise has same shape as base noise
+ if shader_noise.shape != base_noise.shape:
+ # Handle channel dimension mismatches
+ if shader_noise.shape[1] != base_noise.shape[1]:
+ # Create a new tensor with the right number of channels
+ resized_shader = torch.zeros_like(base_noise)
+ # Copy over the common channels
+ min_channels = min(shader_noise.shape[1], base_noise.shape[1])
+ resized_shader[:, :min_channels] = shader_noise[:, :min_channels]
+ return resized_shader
+ else:
+ # Handle any other dimension mismatches with interpolation
+ return torch.nn.functional.interpolate(
+ shader_noise,
+ size=base_noise.shape[2:],
+ mode='bilinear',
+ align_corners=False
+ )
+ return shader_noise
+
+ # Ensure compatible dimensions for blending
+ if shader_noise.shape != base_noise.shape:
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"โ ๏ธ Shape mismatch for blending: base={base_noise.shape}, shader={shader_noise.shape}")
+
+ # Handle channel dimension mismatches
+ if shader_noise.shape[1] != base_noise.shape[1]:
+ # Create a new tensor with the same shape as base_noise
+ resized_shader = torch.zeros_like(base_noise)
+ # Copy over the common channels
+ min_channels = min(shader_noise.shape[1], base_noise.shape[1])
+ resized_shader[:, :min_channels] = shader_noise[:, :min_channels]
+ shader_noise = resized_shader
+
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"โ Matched channel dimensions: {shader_noise.shape}")
+
+ # Handle spatial dimension mismatches with interpolation
+ if shader_noise.shape[2:] != base_noise.shape[2:]:
+ # Use a try/except block to handle any interpolation issues with video tensors
+ try:
+ shader_noise = torch.nn.functional.interpolate(
+ shader_noise,
+ size=base_noise.shape[2:],
+ mode='bilinear',
+ align_corners=False
+ )
+ except RuntimeError as e:
+ if debugger.enabled:
+ print(f"โ ๏ธ Error during interpolation: {e}")
+ # If interpolation fails, reshape tensor to match if possible
+ if len(shader_noise.shape) == 5 and len(base_noise.shape) == 5:
+ # For video tensors, try to reshape directly
+ b, f, c, h, w = shader_noise.shape
+ _, _, target_c, target_h, target_w = base_noise.shape
+ if c != target_c:
+ # Handle channel differences
+ new_shader = torch.zeros_like(base_noise)
+ min_c = min(c, target_c)
+ # Copy each frame separately
+ for i in range(f):
+ new_shader[:, i, :min_c] = shader_noise[:, i, :min_c]
+ shader_noise = new_shader
+ else:
+ # Fall back to zeros if we can't match shapes
+ shader_noise = torch.zeros_like(base_noise)
+
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"โ Matched spatial dimensions: {shader_noise.shape}")
+
+ # Apply the blend mode
+ with debugger.time_operation(f"blend_{blend_mode}") if debugger.enabled else contextlib.nullcontext():
+ if blend_mode == "normal":
+ # Linear interpolation
+ result = base_noise * (1.0 - strength) + shader_noise * strength
+
+ elif blend_mode == "add":
+ # Add shader to base
+ result = base_noise + shader_noise * strength
+
+ elif blend_mode == "multiply":
+ # Multiply base by shader
+ result = base_noise * (1.0 + (shader_noise - 0.5) * strength * 2)
+
+ elif blend_mode == "screen":
+ # Screen blend mode
+ result = 1.0 - (1.0 - base_noise) * (1.0 - shader_noise * strength)
+
+ elif blend_mode == "overlay":
+ # Overlay blend mode
+ mask = base_noise < 0.5
+ result = torch.zeros_like(base_noise)
+ result[mask] = 2 * base_noise[mask] * (shader_noise[mask] * strength)
+ result[~mask] = 1 - 2 * (1 - base_noise[~mask]) * (1 - shader_noise[~mask] * strength)
+
+ elif blend_mode == "soft_light":
+ # Soft light blend mode
+ result = ((1.0 - 2.0 * shader_noise) * base_noise**2 + 2.0 * shader_noise * base_noise) * strength + base_noise * (1.0 - strength)
+
+ elif blend_mode == "hard_light":
+ # Hard light blend mode
+ mask = shader_noise < 0.5
+ result = torch.zeros_like(base_noise)
+ result[mask] = 2 * base_noise[mask] * shader_noise[mask] * strength + base_noise[mask] * (1 - strength)
+ result[~mask] = 1 - 2 * (1 - base_noise[~mask]) * (1 - shader_noise[~mask]) * strength + base_noise[~mask] * (1 - strength)
+
+ elif blend_mode == "difference":
+ # Difference blend mode
+ result = base_noise + (torch.abs(base_noise - shader_noise) * strength)
+
+ else:
+ # Default to normal blend for unknown modes
+ result = base_noise * (1.0 - strength) + shader_noise * strength
+
+ # Debug output for blend results
+ if debugger.enabled:
+ base_stats = {
+ "min": float(base_noise.min().item()),
+ "max": float(base_noise.max().item()),
+ "mean": float(base_noise.mean().item()),
+ "std": float(base_noise.std().item())
+ }
+
+ result_stats = {
+ "min": float(result.min().item()),
+ "max": float(result.max().item()),
+ "mean": float(result.mean().item()),
+ "std": float(result.std().item())
+ }
+
+ if debugger.debug_level >= 2:
+ print(f"๐ Blend stats ({blend_mode}, strength={strength:.2f}):")
+ print(f" Base: min={base_stats['min']:.4f}, max={base_stats['max']:.4f}, mean={base_stats['mean']:.4f}, std={base_stats['std']:.4f}")
+ print(f" Result: min={result_stats['min']:.4f}, max={result_stats['max']:.4f}, mean={result_stats['mean']:.4f}, std={result_stats['std']:.4f}")
+
+ mean_diff = abs(result_stats["mean"] - base_stats["mean"])
+ std_diff = abs(result_stats["std"] - base_stats["std"])
+
+ if mean_diff < 0.001 and std_diff < 0.001:
+ print(f"โ ๏ธ Warning: Blend may not be effective - minimal statistical difference detected")
+ print(f" Base: mean={base_stats['mean']:.4f}, std={base_stats['std']:.4f}")
+ print(f" Result: mean={result_stats['mean']:.4f}, std={result_stats['std']:.4f}")
+
+ return result
+
+ def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
+ denoise=1.0, sequential_stages=2, injection_stages=3, shader_strength=0.3, blend_mode="multiply",
+ noise_transform="none", sequential_distribution="linear_decrease", injection_distribution="linear_decrease",
+ use_temporal_coherence=False, debug_level="0-Off", save_visualizations=False, denoise_visualization_frequency="25% intervals",
+ custom_sigmas=None, # Add custom_sigmas here
+ target_attribute_changes="", # Add target_attribute_changes here
+ fast_high_channel_noise=False, # Add fast_high_channel_noise here
+ show_custom_preview=False, show_default_preview=False, shader_params_override=None):
+ """
+ Run the multi-stage shader noise k-sampler
+ """
+ # Parse debug level from the selected option
+ debug_level_value = int(debug_level.split("-")[0])
+
+ # Set the debug level in the shader debugger
+ debugger = set_debug_level(debug_level_value)
+
+ # Get the visualizer
+ visualizer = get_visualizer()
+
+ # --- Get device early from latent_image ---
+ device = latent_image["samples"].device
+ if debugger.enabled:
+ print(f"โน๏ธ Using device: {device}")
+
+ # Enable the visualizer with the current parameters if save_visualizations is True
+ if save_visualizations:
+ additional_metadata = {
+ "model": getattr(model, 'model_name', "unknown"),
+ "steps": steps,
+ "cfg": cfg,
+ "sampler_name": sampler_name,
+ "scheduler": scheduler,
+ "denoise": denoise,
+ "sequential_stages": sequential_stages,
+ "injection_stages": injection_stages,
+ "shader_strength": shader_strength,
+ "blend_mode": blend_mode,
+ "noise_transform": noise_transform,
+ "sequential_distribution": sequential_distribution,
+ "injection_distribution": injection_distribution,
+ "use_temporal_coherence": use_temporal_coherence,
+ "fast_high_channel_noise": fast_high_channel_noise, # Log the new param
+ }
+
+ # Extract shader type from parameters - use override if provided
+ if shader_params_override is not None:
+ shader_params = shader_params_override
+ else:
+ shader_params = get_shader_params()
+ shader_type = shader_params.get("shader_type", "tensor_field")
+
+ # Enable the visualizer with save_to_output_dir option
+ visualizer.enable(
+ seed=seed,
+ shader_type=shader_type,
+ additional_metadata=additional_metadata
+ )
+
+ if debugger.enabled and debug_level_value >= 1:
+ print(f"๐ Starting shader debugging at level {debug_level}")
+
+ # Log parameters for diagnostics
+ debug_params = {
+ "shader_type": self.shader_type if hasattr(self, 'shader_type') else "unknown",
+ "seed": seed,
+ "sequential_stages": sequential_stages,
+ "injection_stages": injection_stages,
+ "shader_strength": shader_strength,
+ "blend_mode": blend_mode,
+ "noise_transform": noise_transform,
+ "sequential_distribution": sequential_distribution,
+ "injection_distribution": injection_distribution,
+ "use_temporal_coherence": use_temporal_coherence,
+ "debug_level": debug_level
+ }
+ debugger.log_parameters(debug_params)
+
+ # Get model information if available for debugging
+ # Use original model name for metadata/logging consistency
+ model_name = getattr(model, 'model_name', getattr(getattr(model, 'model', None), 'model_name', None))
+
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"โน๏ธ Using model: {model_name if model_name else 'Unknown model'}")
+
+ # Start a new debugging session if enabled
+ if debugger.enabled:
+ debugger.reset()
+ print(f"๐ Starting shader debugging at level {debug_level}")
+
+ # Get the current shader parameters - use override if provided
+ if shader_params_override is not None:
+ shader_params = shader_params_override
+ if debugger.enabled:
+ print("๐ Using provided shader parameters override")
+ else:
+ shader_params = get_shader_params()
+
+ # Extract values for generation
+ shader_type = shader_params.get("shader_type", "tensor_field")
+
+ # --- Parameter Response Mapper Integration --- START ---
+ if target_attribute_changes and target_attribute_changes.strip():
+ if debugger.enabled:
+ print("๐ง Applying Parameter Response Mapper adjustments...")
+ try:
+ import json # Need json for parsing
+ target_attrs = json.loads(target_attribute_changes)
+
+ if isinstance(target_attrs, dict) and target_attrs:
+ # Determine model type for mapper (simple inference)
+ model_name_lower = model_name.lower() if model_name else ""
+ if "sdxl" in model_name_lower:
+ mapper_model_type = "SDXL"
+ else:
+ mapper_model_type = "SD1.5" # Default
+
+ mapper = ParameterResponseMapper(model_type=mapper_model_type)
+
+ # Extract current shader parameters relevant to the mapper
+ current_mapper_params = {
+ p: shader_params.get(p, shader_params.get(f"shader{p.capitalize()}", 0.0))
+ for p in ["scale", "octaves", "warp_strength", "phase_shift"]
+ }
+ # Ensure correct types (e.g., octaves is float for mapper)
+ current_mapper_params["octaves"] = float(current_mapper_params.get("octaves", 3.0))
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f" Mapper using model_type: {mapper_model_type}")
+ print(f" Target Attributes: {target_attrs}")
+ print(f" Current Params for Mapper: {current_mapper_params}")
+
+ # Get recommendations
+ recommendations = mapper.get_recommended_adjustments(
+ target_attrs,
+ current_mapper_params,
+ max_params=2 # Adjust max parameters as needed
+ )
+
+ if recommendations:
+ if debugger.enabled:
+ print(f"โ Mapper recommended adjustments: {recommendations}")
+ # Apply recommendations to the main shader_params
+ for param, value in recommendations.items():
+ # Update both plain and prefixed keys if they exist
+ shader_params[param] = value
+ prefixed_key = f"shader{param.capitalize()}"
+ if prefixed_key in shader_params:
+ shader_params[prefixed_key] = value
+ # Re-log parameters if modified
+ if debugger.enabled:
+ debugger.log_parameters({"shader_params_after_mapper": shader_params})
+ elif debugger.enabled:
+ print("โน๏ธ Mapper provided no recommendations.")
+ else:
+ if debugger.enabled:
+ print("โ ๏ธ Target attribute changes were provided but not a valid dictionary or empty.")
+
+ except Exception as e:
+ print(f"โ Error processing target_attribute_changes: {e}. Skipping mapper adjustments.")
+ # --- Parameter Response Mapper Integration --- END ---
+
+ # Log all parameters if debugging enabled
+ if debugger.enabled:
+ # Collect all parameters for logging
+ all_params = {
+ "shader_type": shader_type,
+ "seed": seed,
+ "steps": steps,
+ "cfg": cfg,
+ "sampler_name": sampler_name,
+ "scheduler": scheduler,
+ "denoise": denoise,
+ "sequential_stages": sequential_stages,
+ "injection_stages": injection_stages,
+ "shader_strength": shader_strength,
+ "blend_mode": blend_mode,
+ "noise_transform": noise_transform,
+ "sequential_distribution": sequential_distribution,
+ "injection_distribution": injection_distribution,
+ "use_temporal_coherence": use_temporal_coherence,
+ "fast_high_channel_noise": fast_high_channel_noise, # Log the new param
+ "shader_params": shader_params
+ }
+ debugger.log_parameters(all_params)
+
+ # Determine which model object to use (original or wrapper)
+ model_to_use = model
+ using_custom_sigmas = False
+ total_sampling_steps = steps # Default to input steps
+
+ if custom_sigmas is not None:
+ if debugger.enabled:
+ print("โน๏ธ Attempting to use custom sigma schedule.")
+ # Basic validation: Check if it's a tensor and has values
+ if isinstance(custom_sigmas, torch.Tensor) and custom_sigmas.numel() > 1:
+ try:
+ # Pass the determined device to the wrapper
+ model_to_use = CustomSigmaModelWrapper(model, custom_sigmas, device)
+ using_custom_sigmas = True
+ actual_sigma_count = len(model_to_use.model_sampling.sigmas)
+
+ if actual_sigma_count > 1:
+ total_sampling_steps = actual_sigma_count - 1 # Derive steps from sigmas
+ if debugger.enabled:
+ print(f"โ Successfully wrapped model with {actual_sigma_count} custom sigmas.")
+ print(f"โน๏ธ Overriding sampling steps. Using {total_sampling_steps} steps based on custom sigmas (Input steps: {steps}).")
+ else:
+ # Handle case of single sigma value - technically invalid for sampling
+ print(f"โ ๏ธ Warning: Custom sigma tensor has only {actual_sigma_count} value(s). Cannot derive steps. Using input steps {steps}.")
+ using_custom_sigmas = False # Revert flag if sigmas are invalid for steps
+ model_to_use = model # Revert model
+ total_sampling_steps = steps # Fallback to input steps
+
+ # Remove previous sigma length vs steps comparison logic as we now override steps
+ # if actual_sigma_count < steps: ...
+ # elif actual_sigma_count != expected_sigma_count: ...
+
+ except Exception as e:
+ print(f"โ Error wrapping model with custom sigmas: {e}. Falling back to default model sigmas and input steps.")
+ model_to_use = model # Fallback
+ using_custom_sigmas = False
+ total_sampling_steps = steps # Fallback
+ else:
+ if debugger.enabled:
+ print(f"โ ๏ธ Custom sigmas input is not a valid tensor or has <= 1 value. Falling back to default model sigmas and input steps.")
+ model_to_use = model # Fallback
+ using_custom_sigmas = False
+ total_sampling_steps = steps # Fallback
+ elif debugger.enabled:
+ print(f"โน๏ธ Using model's default sigma schedule and {steps} input steps.")
+
+
+ # If shader_strength is 0 or both stage types are 0, use the standard ksampler instead
+ if shader_strength == 0.0 or (sequential_stages <= 0 and injection_stages <= 0):
+ if debugger.enabled:
+ print(f"โน๏ธ Using standard ksampler (no shader stages enabled or zero strength) with {total_sampling_steps} steps.")
+ # IMPORTANT: Pass model_to_use and the potentially overridden total_sampling_steps
+ samples = common_ksampler(model_to_use, seed, total_sampling_steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise)[0]
+
+ # Save final output if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.capture_final_result(samples["samples"], {"method": "standard_ksampler", "stages": 0, "total_steps": total_sampling_steps})
+ visualizer.disable()
+
+ return {"ui": {"images": [], "show_custom_preview": [show_custom_preview]}, "result": (samples,)}
+
+ # Determine if we're working with a video or an image
+ latent_shape = latent_image["samples"].shape
+ device = latent_image["samples"].device
+ is_video = len(latent_shape) == 5
+ batch_size = latent_shape[0]
+
+ if debugger.enabled:
+ print(f"โน๏ธ Total steps for sampling process: {total_sampling_steps} (derived from {'custom sigmas' if using_custom_sigmas else 'input'})")
+
+ # Determine the target channel count early on using the model
+ # Use model_to_use here to correctly get channels if model was wrapped (though usually wrapper forwards this)
+ print("โน๏ธ Determining target channel count for the model...")
+ target_channels = self.get_model_channel_count(model_to_use)
+
+ # Initialize dimension indices and counts
+ frames = 1
+ channels = target_channels # Start assuming model's count
+ height = latent_shape[-2]
+ width = latent_shape[-1]
+ channel_dim_idx = -1
+ frame_dim_idx = -1
+ detected_format = "Unknown"
+
+ if is_video:
+ latent_dim1 = latent_shape[1]
+ latent_dim2 = latent_shape[2]
+
+ # Logic to determine Frame vs Channel dimension for 5D tensor
+ # Priority 1: If one dimension matches latent shape and the other matches model channels
+ if latent_dim1 == target_channels and latent_dim2 != target_channels:
+ # Format likely [B, C, F, H, W]
+ channels = latent_dim1
+ frames = latent_dim2
+ channel_dim_idx = 1
+ frame_dim_idx = 2
+ detected_format = "[B, C, F, H, W]"
+ elif latent_dim2 == target_channels and latent_dim1 != target_channels:
+ # Format likely [B, F, C, H, W]
+ channels = latent_dim2
+ frames = latent_dim1
+ channel_dim_idx = 2
+ frame_dim_idx = 1
+ detected_format = "[B, F, C, H, W]"
+ # Priority 2: If both match target_channels (ambiguous, default to B,F,C,H,W)
+ elif latent_dim1 == target_channels and latent_dim2 == target_channels:
+ # Could be B,C,F or B,F,C - Assume B,F,C common format
+ print(f"โ ๏ธ Ambiguous video shape {latent_shape} where both dim 1 & 2 match target channels {target_channels}. Assuming [B, F, C, H, W] format.")
+ channels = target_channels
+ frames = latent_dim1 # Use dim 1 as frames
+ channel_dim_idx = 2
+ frame_dim_idx = 1
+ detected_format = "[B, F, C, H, W] (Ambiguous)"
+ # Priority 3: If neither matches target_channels (Fallback, assume B,F,C,H,W)
+ else:
+ # print(f"โ ๏ธ Video shape {latent_shape} doesn't match target channels {target_channels} in dim 1 or 2. Assuming [B, F, C, H, W] format and using model's target channels.")
+ channels = target_channels # Use model's target channels
+ frames = latent_dim1 # Assume dim 1 is frames
+ channel_dim_idx = 2
+ frame_dim_idx = 1
+ detected_format = "[B, F, C, H, W] (Fallback)"
+
+ else: # 4D Tensor
+ # Assume [B, C, H, W] format
+ channels = latent_shape[1]
+ frames = 1
+ channel_dim_idx = 1
+ frame_dim_idx = -1 # No frame dimension
+ detected_format = "[B, C, H, W]"
+ # Validate against model channels
+ if channels != target_channels:
+ print(f"โ ๏ธ Image shape {latent_shape} channel count ({channels}) differs from model's target channels ({target_channels}). Using model's target channels for noise generation.")
+ channels = target_channels # Prioritize model's expectation for noise
+
+ if debugger.enabled:
+ print(f"โน๏ธ Detected Format: {detected_format}")
+ print(f"โน๏ธ Latent Shape: {latent_shape}")
+ print(f"โน๏ธ Target Channels (Model): {target_channels}")
+ print(f"โน๏ธ Deduced Dimensions: Frames={frames}, Channels={channels}, Height={height}, Width={width}")
+ print(f"โน๏ธ Deduced Indices: Frame Dim={frame_dim_idx}, Channel Dim={channel_dim_idx}")
+
+ # Construct the target noise shape using detected dimensions and model's target channels
+ # Order matters based on detected format!
+ if is_video:
+ if detected_format.startswith("[B, C, F"):
+ # [B, C, F, H, W] format
+ noise_shape = (batch_size, target_channels, frames, height, width)
+ else:
+ # Defaulting to [B, F, C, H, W] format
+ noise_shape = (batch_size, frames, target_channels, height, width)
+ else:
+ # [B, C, H, W] format
+ noise_shape = (batch_size, target_channels, height, width)
+
+ if debugger.enabled:
+ print(f"โน๏ธ Target Noise Shape: {noise_shape} (based on detected format and model channels)")
+
+ # Check if input latent channels match target noise channels before proceeding
+ latent_channel_dim_idx = channel_dim_idx if is_video else 1
+ if latent_shape[latent_channel_dim_idx] != noise_shape[latent_channel_dim_idx]:
+ # This check is crucial as resizing happens later in shader_ksampler
+ # print(f"โ ๏ธ Input latent channel count ({latent_shape[latent_channel_dim_idx]}) differs from target noise channel count ({noise_shape[latent_channel_dim_idx]}). Noise resizing will occur in ksampler.")
+ pass
+ elif debugger.enabled:
+ print(f"โ Input latent channel count matches target noise channels ({target_channels}).")
+
+ if debugger.enabled:
+ print(f"โน๏ธ Processing {'video' if is_video else 'image'} with shape {latent_shape}")
+ print(f"โน๏ธ Model detected requires {target_channels} channels.")
+
+ # Handle temporal coherence
+ # Add time parameter if not present
+ shader_params["time"] = shader_params.get("time", 0.0)
+ # Make sure we have a base_seed for temporal coherence
+ shader_params["base_seed"] = seed
+ # Add the temporal coherence setting to shader params
+ shader_params["useTemporalCoherence"] = use_temporal_coherence
+ shader_params["temporal_coherence"] = use_temporal_coherence # Ensure this alternative key is also set
+ # Add the fast high channel noise setting to shader params
+ shader_params["fast_high_channel_noise"] = fast_high_channel_noise
+
+ if debugger.enabled and use_temporal_coherence:
+ print(f"๐ Using temporal coherence with base seed {seed}")
+
+ # Add print statements for the requested parameters
+ print(f"ShaderNoiseKSampler: Using shader_type: {shader_type}")
+ print(f"ShaderNoiseKSampler: Shape Mask: type={shader_params.get('shaderShapeType', shader_params.get('shape_type', 'none'))}, strength={shader_params.get('shaderShapeStrength', shader_params.get('shapemaskstrength', 1.0))}")
+ print(f"ShaderNoiseKSampler: Color Scheme: scheme={shader_params.get('colorScheme', 'none')}, intensity={shader_params.get('shaderColorIntensity', shader_params.get('intensity', 0.8))}")
+ print(f"ShaderNoiseKSampler: Scale={shader_params.get('shaderScale', shader_params.get('scale', 1.0))}")
+ print(f"ShaderNoiseKSampler: Octaves={shader_params.get('shaderOctaves', shader_params.get('octaves', 3.0))}")
+ print(f"ShaderNoiseKSampler: Warp Strength={shader_params.get('shaderWarpStrength', shader_params.get('warp_strength', 0.5))}")
+ print(f"ShaderNoiseKSampler: Phase Shift={shader_params.get('shaderPhaseShift', shader_params.get('phase_shift', 0.5))}")
+
+ # Set initial random seed for consistency
+ torch.manual_seed(seed)
+
+ # Extract the latent samples
+ latent_samples = latent_image["samples"].clone()
+
+ # Generate initial noise if using full denoise
+ if denoise > 0.0:
+ if debugger.enabled:
+ print(f"๐ฒ Generating initial noise with seed {seed}")
+
+ # Generate noise with the CORRECT noise_shape derived above
+ initial_noise = torch.randn(noise_shape, device=device)
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ debugger.analyze_tensor(initial_noise, "initial_noise")
+
+ # Save initial noise if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ initial_noise.clone(),
+ "Initial Random Noise",
+ stage_info={"stage_type": "startup"}
+ )
+ else:
+ # No noise needed for zero denoise
+ initial_noise = None
+ if debugger.enabled:
+ print("โน๏ธ No initial noise needed (denoise=0)")
+
+ # Current latent state
+ current_latent = {"samples": latent_samples}
+
+ # Save initial latent if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ latent_samples.clone(),
+ "Input Latent",
+ stage_info={"stage_type": "startup"},
+ is_sample=True
+ )
+
+ # PHASE 1: Sequential Shader Stages
+ # Only proceed if we have sequential stages to run
+ if sequential_stages > 0:
+ if debugger.enabled:
+ print(f"๐ Starting Sequential Phase with {sequential_stages} stages over {total_sampling_steps} total steps")
+
+ # Calculate the strength for each sequential stage
+ sequential_strengths = self._calculate_stage_strengths(shader_strength, sequential_stages, sequential_distribution)
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f"๐ช Sequential strengths: {[f'{s:.3f}' for s in sequential_strengths]}")
+
+ # Calculate the step ranges for each sequential stage based on total_sampling_steps
+ sequential_ranges = self._calculate_step_ranges(total_sampling_steps, sequential_stages)
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f"๐ข Sequential step ranges: {sequential_ranges}")
+
+ # Generate base noise for blending for sequential stages
+ torch.manual_seed(seed)
+
+ if debugger.enabled:
+ print(f"๐ฒ Generating base sequential noise with seed {seed}")
+
+ # Generate noise with the CORRECT noise_shape derived above
+ base_noise_sequential = torch.randn(noise_shape, device=device)
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ debugger.analyze_tensor(base_noise_sequential, "base_noise_sequential")
+
+ # Save base sequential noise if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ base_noise_sequential.clone(),
+ "Base Sequential Noise",
+ stage_info={"stage_type": "sequential_base"}
+ )
+
+ # Process each sequential stage
+ for stage_idx, (start_step, end_step) in enumerate(sequential_ranges):
+ # Start logging this stage
+ if debugger.enabled:
+ debugger.log_stage_start("sequential", stage_idx, {
+ "start_step": start_step,
+ "end_step": end_step,
+ "strength": sequential_strengths[stage_idx]
+ })
+
+ # Calculate stage-specific parameters
+ stage_steps = end_step - start_step # Steps for this specific stage call
+ stage_strength = sequential_strengths[stage_idx]
+
+ # Update shader parameters for this stage
+ stage_shader_params = shader_params.copy()
+ stage_shader_params["stage"] = stage_idx
+ # Ensure temporal coherence setting is explicitly added to stage params
+ stage_shader_params["useTemporalCoherence"] = use_temporal_coherence
+
+ # Calculate denoising strength for this stage (using total_sampling_steps)
+ # Note: This might not be strictly necessary if shader_ksampler ignores denoise with force_full_denoise=True
+ # but let's keep it consistent for potential future changes or logging.
+ progress_done = start_step / total_sampling_steps if total_sampling_steps > 0 else 0
+ # Use the original denoise value as the upper limit for the first stage
+ max_denoise_for_stage = denoise if start_step == 0 else 1.0
+ stage_denoise = max(0.0, max_denoise_for_stage - progress_done) * (total_sampling_steps / stage_steps if stage_steps > 0 else 1.0) # Rescale denoise? Let's simplify.
+ # Simpler approach: Assume each stage denoises fully relative to its start point.
+ # The denoise=1.0 passed to shader_ksampler combined with force_full_denoise=True handles this.
+ stage_denoise_param_for_sampler = 1.0
+
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"๐ Sequential stage {stage_idx}: steps {start_step}-{end_step} ({stage_steps} steps this stage), strength {stage_strength:.3f}")
+
+ # Determine seed for this stage based on consistency mode
+ stage_seed = seed if use_temporal_coherence else seed + stage_idx
+
+ # Generate shader noise for this stage
+ if debugger.enabled:
+ print(f"๐ณ Generating shader noise for sequential stage {stage_idx}")
+
+ with debugger.time_operation(f"gen_seq_shader_{stage_idx}") if debugger.enabled else contextlib.nullcontext():
+ stage_shader_noise = self._generate_shader_noise(
+ latent_samples=latent_samples, # Pass latent samples to derive shape if needed
+ target_noise_shape=noise_shape, # Pass the target noise shape
+ shader_params=stage_shader_params,
+ shader_type=shader_type,
+ seed=stage_seed,
+ device=device,
+ model=model_to_use,
+ model_name=model_name,
+ frame_count=frames, # Pass correct frame count
+ frame_dim_idx=frame_dim_idx # Pass frame dimension index
+ )
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ debugger.analyze_tensor(stage_shader_noise, f"seq_shader_noise_{stage_idx}")
+
+ # Save shader noise if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ stage_shader_noise.clone(),
+ f"Sequential {stage_idx} Shader Noise",
+ stage_info={"stage_type": "sequential", "stage_idx": stage_idx}
+ )
+
+ # Apply noise transform
+ if debugger.enabled and noise_transform != "none":
+ print(f"๐ Applying {noise_transform} transform to noise")
+
+ stage_shader_noise = self._apply_noise_transform(stage_shader_noise, noise_transform)
+
+ if debugger.enabled and debugger.debug_level >= 3 and noise_transform != "none":
+ debugger.analyze_tensor(stage_shader_noise, f"seq_transformed_noise_{stage_idx}")
+
+ # Save transformed noise if visualizer is enabled and transform was applied
+ if visualizer.enabled and noise_transform != "none":
+ visualizer.save_latent_visualization(
+ stage_shader_noise.clone(),
+ f"Sequential {stage_idx} Transformed Noise",
+ stage_info={"stage_type": "sequential", "stage_idx": stage_idx, "transform": noise_transform}
+ )
+
+ # Blend with base noise
+ if debugger.enabled:
+ print(f"๐ Blending noise with {blend_mode} mode at strength {stage_strength:.3f}")
+
+ final_noise = self._blend_noises(base_noise_sequential, stage_shader_noise, blend_mode, stage_strength)
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ debugger.log_blend_operation(base_noise_sequential, stage_shader_noise, final_noise, blend_mode, stage_strength)
+
+ # Save blended noise if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ final_noise.clone(),
+ f"Sequential {stage_idx} Blended Noise",
+ stage_info={"stage_type": "sequential", "stage_idx": stage_idx, "blend_mode": blend_mode, "strength": stage_strength}
+ )
+
+ # Create a copy of current latent for sampling
+ stage_latent = current_latent.copy()
+
+ # Run the sampling for this stage
+ stage_seed = seed if use_temporal_coherence else seed + stage_idx
+
+ if debugger.enabled:
+ print(f"๐ Running sequential sampling stage {stage_idx} with seed {stage_seed}")
+ if using_custom_sigmas:
+ print(" (Using custom sigma schedule)")
+
+ # Create stage info for this sequential stage
+ sequential_stage_info = {
+ "stage_type": "sequential",
+ "stage_idx": stage_idx,
+ "start_step": start_step,
+ "end_step": end_step,
+ "strength": stage_strength,
+ "denoise": stage_denoise
+ }
+
+ stage_result = shader_ksampler(
+ model=model_to_use, # Pass the potentially wrapped model
+ seed=stage_seed,
+ steps=stage_steps, # Pass the steps for THIS stage
+ cfg=cfg,
+ sampler_name=sampler_name,
+ scheduler=scheduler,
+ positive=positive,
+ negative=negative,
+ latent_image=stage_latent,
+ noise_tensor=final_noise,
+ denoise=stage_denoise_param_for_sampler, # Pass 1.0 as it does full denoise per stage
+ stage_info=sequential_stage_info,
+ denoise_visualization_frequency=denoise_visualization_frequency
+ )
+
+ # --- DEBUG PRINT ---
+ # print(f"[DEBUG] shader_ksampler returned (type: {type(stage_result)}): {stage_result}")
+ # --- END DEBUG PRINT ---
+
+ # Update current latent with stage result - MODIFIED TO BE SAFER
+ if isinstance(stage_result, tuple) and len(stage_result) > 0 and isinstance(stage_result[0], dict):
+ current_latent = stage_result[0]
+ elif isinstance(stage_result, dict) and 'samples' in stage_result:
+ print("[DEBUG] Handling shader_ksampler result as dict in sequential loop.")
+ current_latent = stage_result # Assign dict directly
+ else:
+ print(f"[ERROR] Unexpected stage_result type or format in sequential loop: {type(stage_result)}. Content: {stage_result}")
+ raise TypeError(f"shader_ksampler returned unexpected type/format: {type(stage_result)}")
+
+ # Capture shader process if visualizer is enabled
+ if visualizer.enabled:
+ stage_data = {
+ "start_step": start_step,
+ "end_step": end_step,
+ "strength": stage_strength,
+ "denoise": stage_denoise,
+ "seed": stage_seed
+ }
+
+ visualizer.capture_shader_process(
+ phase="sequential",
+ stage_idx=stage_idx,
+ stage_type="shader_sampling",
+ stage_data=stage_data,
+ base_noise=base_noise_sequential.clone(),
+ shader_noise=stage_shader_noise.clone(),
+ blended_noise=final_noise.clone(),
+ result=current_latent["samples"].clone()
+ )
+
+ if debugger.enabled:
+ debugger.log_stage_end("sequential", stage_idx)
+ if debugger.debug_level >= 2:
+ debugger.analyze_tensor(current_latent["samples"], f"seq_stage{stage_idx}_result")
+
+ # PHASE 2: Injection Shader Stages S{ฮฑi}โK{ฮฒi}
+ # Only proceed if we have injection stages to run
+ if injection_stages > 0:
+ if debugger.enabled:
+ print(f"๐ Starting Injection Phase with {injection_stages} stages over {total_sampling_steps} total steps")
+
+ # If we skipped sequential stages, current_latent might not be set properly
+ if sequential_stages <= 0 and denoise > 0.0:
+ # Run a standard ksampler for initial denoising using total_sampling_steps
+ if debugger.enabled:
+ print(f"๐ Performing initial denoising before injection stages ({total_sampling_steps} steps)")
+
+ initial_result = shader_ksampler(
+ model=model_to_use, # Pass the potentially wrapped model
+ seed=seed,
+ steps=total_sampling_steps, # Use total_sampling_steps
+ cfg=cfg,
+ sampler_name=sampler_name,
+ scheduler=scheduler,
+ positive=positive,
+ negative=negative,
+ latent_image={"samples": latent_samples},
+ noise_tensor=initial_noise,
+ denoise=denoise, # Use the overall denoise parameter here
+ stage_info={
+ "stage_type": "initial_denoising",
+ "stage_idx": 0,
+ "start_step": 0,
+ "end_step": total_sampling_steps,
+ "denoise": denoise
+ },
+ denoise_visualization_frequency=denoise_visualization_frequency
+ ) # Call first, then check type
+
+ # Check result type - MODIFIED TO BE SAFER
+ if isinstance(initial_result, tuple) and len(initial_result) > 0 and isinstance(initial_result[0], dict):
+ current_latent = initial_result[0]
+ elif isinstance(initial_result, dict) and 'samples' in initial_result:
+ print("[DEBUG] Handling initial shader_ksampler result as dict.")
+ current_latent = initial_result # Assign dict directly
+ else:
+ print(f"[ERROR] Unexpected initial_result type or format: {type(initial_result)}. Content: {initial_result}")
+ raise TypeError(f"Initial shader_ksampler returned unexpected type/format: {type(initial_result)}")
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ debugger.analyze_tensor(current_latent["samples"], "after_initial_denoising")
+
+ # Save initial denoising result if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ current_latent["samples"].clone(),
+ "Initial Denoising Result",
+ stage_info={"stage_type": "initial_denoising"},
+ is_sample=True
+ )
+
+ # Calculate the strength for each injection stage
+ injection_strengths = self._calculate_stage_strengths(shader_strength, injection_stages, injection_distribution)
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f"๐ช Injection strengths: {[f'{s:.3f}' for s in injection_strengths]}")
+
+ # Calculate at which steps to apply shader noise for injection based on total_sampling_steps
+ step_points = self._calculate_step_points(total_sampling_steps, injection_stages)
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f"๐ข Injection step points: {step_points}")
+
+ # Generate a base noise pattern for blending in injection stages
+ # Use the same seed if temporal coherence is enabled
+ injection_base_seed = seed if use_temporal_coherence else seed + sequential_stages
+
+ if debugger.enabled:
+ print(f"๐ฒ Generating base injection noise with seed {injection_base_seed}")
+
+ torch.manual_seed(injection_base_seed)
+ # Generate noise with the CORRECT noise_shape derived above
+ base_noise_injection = torch.randn(noise_shape, device=device)
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ debugger.analyze_tensor(base_noise_injection, "base_noise_injection")
+
+ # Generate base shader noise for injection stages
+ base_shader_noise_injection = self._generate_shader_noise(
+ latent_samples=latent_samples, # Pass latent samples
+ target_noise_shape=noise_shape, # Pass the target noise shape
+ shader_params=shader_params,
+ shader_type=shader_type,
+ seed=seed,
+ device=device,
+ model=model_to_use,
+ model_name=model_name,
+ frame_count=frames, # Pass correct frame count
+ frame_dim_idx=frame_dim_idx # Pass frame dimension index
+ )
+
+ # Save base shader noise for injection if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ base_shader_noise_injection.clone(),
+ "Base Injection Shader Noise",
+ stage_info={"stage_type": "injection_base_shader"}
+ )
+
+ # Create step ranges for injection stages based on total_sampling_steps
+ injection_ranges = []
+ for i in range(len(step_points)):
+ start_step = step_points[i]
+ # For last stage, go to the end (total_sampling_steps)
+ end_step = step_points[i + 1] if i < len(step_points) - 1 else total_sampling_steps
+ # Only add if there are steps to process
+ if end_step > start_step:
+ injection_ranges.append((start_step, end_step))
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ print(f"๐ข Injection ranges: {injection_ranges}")
+
+ # Process each injection stage
+ for stage_idx, (start_step, end_step) in enumerate(injection_ranges):
+ # Start logging this stage
+ if debugger.enabled:
+ debugger.log_stage_start("injection", stage_idx, {
+ "start_step": start_step,
+ "end_step": end_step,
+ "strength": injection_strengths[stage_idx]
+ })
+
+ # Calculate stage-specific parameters
+ stage_steps = end_step - start_step # Steps for this specific stage call
+ stage_strength = injection_strengths[stage_idx]
+
+ # Calculate remaining denoise for this stage (see sequential stage comment)
+ progress_done = start_step / total_sampling_steps if total_sampling_steps > 0 else 0
+ max_denoise_for_stage = denoise if start_step == 0 else 1.0 # Likely always 1.0 here
+ stage_denoise_param_for_sampler = 1.0 # Pass 1.0 as it does full denoise per stage
+
+ if debugger.enabled and debugger.debug_level >= 1:
+ print(f"๐ Injection stage {stage_idx}: steps {start_step}-{end_step} ({stage_steps} steps this stage), strength {stage_strength:.3f}")
+
+ # Use base shader noise but apply stage-specific transformations
+ shader_noise = base_shader_noise_injection.clone()
+
+ if debugger.enabled and noise_transform != "none":
+ print(f"๐ Applying {noise_transform} transform to injection noise")
+
+ shader_noise = self._apply_noise_transform(shader_noise, noise_transform)
+
+ if debugger.enabled and debugger.debug_level >= 3 and noise_transform != "none":
+ debugger.analyze_tensor(shader_noise, f"inj_transformed_noise_{stage_idx}")
+
+ # Save transformed noise if visualizer is enabled and transform was applied
+ if visualizer.enabled and noise_transform != "none":
+ visualizer.save_latent_visualization(
+ shader_noise.clone(),
+ f"Injection {stage_idx} Transformed Noise",
+ stage_info={"stage_type": "injection", "stage_idx": stage_idx, "transform": noise_transform}
+ )
+
+ # Blend with base noise
+ if debugger.enabled:
+ print(f"๐ Blending injection noise with {blend_mode} mode at strength {stage_strength:.3f}")
+
+ final_noise = self._blend_noises(base_noise_injection, shader_noise, blend_mode, stage_strength)
+
+ if debugger.enabled and debugger.debug_level >= 2:
+ debugger.log_blend_operation(base_noise_injection, shader_noise, final_noise, blend_mode, stage_strength)
+
+ # Save blended noise if visualizer is enabled
+ if visualizer.enabled:
+ visualizer.save_latent_visualization(
+ final_noise.clone(),
+ f"Injection {stage_idx} Blended Noise",
+ stage_info={"stage_type": "injection", "stage_idx": stage_idx, "blend_mode": blend_mode, "strength": stage_strength}
+ )
+
+ # Create a copy of current latent for sampling
+ stage_latent = current_latent.copy()
+
+ # Run the sampling for this injection stage
+ stage_seed = seed if use_temporal_coherence else seed + sequential_stages + stage_idx
+
+ if debugger.enabled:
+ print(f"๐ Running injection sampling stage {stage_idx} with seed {stage_seed}")
+ if using_custom_sigmas:
+ print(" (Using custom sigma schedule)")
+
+ # Create stage info for this injection stage
+ injection_stage_info = {
+ "stage_type": "injection",
+ "stage_idx": stage_idx,
+ "start_step": start_step,
+ "end_step": end_step,
+ "strength": stage_strength,
+ "denoise": stage_denoise_param_for_sampler
+ }
+
+ stage_result = shader_ksampler(
+ model=model_to_use, # Pass the potentially wrapped model
+ seed=stage_seed,
+ steps=stage_steps, # Pass the steps for THIS stage
+ cfg=cfg,
+ sampler_name=sampler_name,
+ scheduler=scheduler,
+ positive=positive,
+ negative=negative,
+ latent_image=stage_latent,
+ noise_tensor=final_noise,
+ denoise=stage_denoise_param_for_sampler, # Pass 1.0
+ stage_info=injection_stage_info,
+ denoise_visualization_frequency=denoise_visualization_frequency
+ )
+
+ # Update current latent with stage result - MODIFIED TO BE SAFER
+ if isinstance(stage_result, tuple) and len(stage_result) > 0 and isinstance(stage_result[0], dict):
+ current_latent = stage_result[0]
+ elif isinstance(stage_result, dict) and 'samples' in stage_result:
+ print("[DEBUG] Handling shader_ksampler result as dict in injection loop.")
+ current_latent = stage_result # Assign dict directly
+ else:
+ print(f"[ERROR] Unexpected stage_result type or format in injection loop: {type(stage_result)}. Content: {stage_result}")
+ raise TypeError(f"shader_ksampler returned unexpected type/format: {type(stage_result)}")
+
+ # Capture shader process if visualizer is enabled
+ if visualizer.enabled:
+ stage_data = {
+ "start_step": start_step,
+ "end_step": end_step,
+ "strength": stage_strength,
+ "denoise": stage_denoise_param_for_sampler,
+ "seed": stage_seed
+ }
+
+ visualizer.capture_shader_process(
+ phase="injection",
+ stage_idx=stage_idx,
+ stage_type="shader_sampling",
+ stage_data=stage_data,
+ base_noise=base_noise_injection.clone(),
+ shader_noise=shader_noise.clone(),
+ blended_noise=final_noise.clone(),
+ result=current_latent["samples"].clone()
+ )
+
+ if debugger.enabled:
+ debugger.log_stage_end("injection", stage_idx)
+ if debugger.debug_level >= 2:
+ debugger.analyze_tensor(current_latent["samples"], f"inj_stage{stage_idx}_result")
+
+ # Reset random seed state when done
+ torch.manual_seed(torch.seed())
+
+ # Format output
+ out = latent_image.copy()
+ out["samples"] = current_latent["samples"]
+
+ # Include shader parameters in the UI information
+ shader_info = {
+ "shader_type": shader_type,
+ "shader_strength": shader_strength,
+ "sequential_stages": sequential_stages,
+ "injection_stages": injection_stages,
+ "sequential_distribution": sequential_distribution,
+ "injection_distribution": injection_distribution,
+ "noise_transform": noise_transform,
+ "temporal_coherence": use_temporal_coherence
+ }
+ shader_info["using_custom_sigmas"] = using_custom_sigmas # Add info about sigma usage
+
+ # Capture final result if visualizer is enabled - this doesn't affect the output
+ if visualizer.enabled:
+ final_metadata = {
+ "shader_type": shader_type,
+ "sequential_stages": sequential_stages,
+ "injection_stages": injection_stages,
+ "blend_mode": blend_mode,
+ "noise_transform": noise_transform
+ }
+ # Use a copy of the tensor to ensure we don't modify the output
+ visualizer.capture_final_result(current_latent["samples"].clone(), final_metadata)
+ # Get visualization paths for UI display
+ viz_paths = visualizer.get_ui_image_paths()
+ visualizer.disable()
+ else:
+ viz_paths = {
+ "base_noise": None,
+ "shader_noise": None,
+ "blended_noise": None,
+ "stage_results": None,
+ "final_result": None,
+ "grids": []
+ }
+
+ # --- Explicitly delete the model reference before returning ---
+ # This helps ensure the wrapper (if created) is released promptly.
+ if 'model_to_use' in locals():
+ del model_to_use
+ # --- End explicit deletion ---
+
+ # Return both the generated samples and UI information - visualization does not affect this
+ return {"ui": {
+ "images": [],
+ "show_custom_preview": [show_custom_preview],
+ "shader_info": shader_info,
+ "viz_paths": viz_paths # Add visualization paths to UI data
+ }, "result": (out,)}
diff --git a/shader_params_reader.py b/shader_params_reader.py
new file mode 100644
index 0000000..a9d3d5e
--- /dev/null
+++ b/shader_params_reader.py
@@ -0,0 +1,1315 @@
+import json
+import os
+import torch
+import math
+
+class ShaderParamsReader:
+ """
+ Class for reading and applying shader parameters from JSON file
+ Implements the Lt=Sฮฑ(N)โKฮฒ(t) pattern where shader transforms are applied to noise before sampling
+ """
+
+ @staticmethod
+ def smoothstep(edge0, edge1, x):
+ """
+ GLSL-style smoothstep.
+ """
+ # Ensure edges are tensors for broadcasting with x
+ if not isinstance(edge0, torch.Tensor):
+ edge0 = torch.full_like(x, float(edge0), device=x.device, dtype=x.dtype)
+ if not isinstance(edge1, torch.Tensor):
+ edge1 = torch.full_like(x, float(edge1), device=x.device, dtype=x.dtype)
+
+ # Calculate t, handling potential edge0 >= edge1 cases by clamping
+ delta = edge1 - edge0
+ # Avoid division by zero or near-zero, maintain sign for correct 1-smoothstep
+ safe_delta = torch.where(torch.abs(delta) < 1e-8, torch.sign(delta) * 1e-8 + 1e-8*(1-torch.abs(torch.sign(delta))), delta)
+ t = torch.clamp((x - edge0) / safe_delta, 0.0, 1.0)
+
+ return t * t * (3.0 - 2.0 * t)
+
+ @staticmethod
+ def random_val(coords, base_seed, seed_offset):
+ """
+ Generate a random-like value based on coordinates and seed.
+ Matches the random_val helper in CurlNoiseGenerator.
+ """
+ # Ensure base_seed and seed_offset are appropriate for torch.manual_seed
+ # torch.manual_seed expects an integer.
+ current_seed = int(base_seed) + int(seed_offset)
+ torch.manual_seed(current_seed)
+
+ # Use a simple hash-like function based on coordinates
+ # Ensuring coords are float for calculations
+ coords_float = coords.float()
+ hash_val = torch.sin(coords_float[:, :, :, 0] * (12.9898 + seed_offset) + coords_float[:, :, :, 1] * (78.233 + seed_offset)) * 43758.5453
+ return torch.frac(hash_val)
+
+ @staticmethod
+ def get_shader_params(custom_path=None):
+ """
+ Utility function to read shader parameters from file.
+ Returns a dictionary of shader parameters.
+
+ Args:
+ custom_path: Optional path to a custom JSON file
+
+ Returns:
+ Dictionary of shader parameters
+ """
+ # Default values in case file doesn't exist or is invalid
+ default_params = {
+ "shader_type": "tensor_field",
+ "visualization_type": 3, # ellipses
+ "scale": 1.0,
+ "phase_shift": 0.0,
+ "warp_strength": 0.5,
+ "time": 0.0,
+ "octaves": 3.0,
+ "intensity": 0.8, # influence/strength of the shader
+ "shapemaskstrength": 1.0, # strength of the shape mask
+ "shape_type": "none" # type of shape mask
+ }
+
+ # Get the extension directory (where this file is located)
+ EXTENSION_DIR = os.path.dirname(os.path.abspath(__file__))
+
+ # Path to the shader_params.json file (default or custom)
+ if custom_path:
+ params_file = custom_path
+ else:
+ # Try to find params in root directory first
+ params_file = os.path.join(EXTENSION_DIR, "shader_params.json")
+
+ # If not found, try the data folder
+ if not os.path.exists(params_file):
+ params_file = os.path.join(EXTENSION_DIR, "data", "shader_params.json")
+
+ # Try to read the shader parameters from the file
+ try:
+ if os.path.exists(params_file):
+ # print(f"Found parameters file: {params_file}")
+ with open(params_file, 'r') as f:
+ loaded_params = json.load(f)
+
+ # Map between different parameter naming conventions
+ param_mapping = {
+ "shaderType": "shader_type",
+ "shaderScale": "scale",
+ "shaderOctaves": "octaves",
+ "shaderWarpStrength": "warp_strength",
+ "shaderPhaseShift": "phase_shift",
+ "shapeMaskStrength": "shapemaskstrength",
+ "shaderShapeStrength": "shapemaskstrength",
+ "shaderShapeType": "shape_type"
+ }
+
+ # Output raw loaded params for debugging
+ # print(f"Raw JSON params: {loaded_params}")
+
+ # Convert parameter names if needed
+ params = {}
+ for key, value in loaded_params.items():
+ if key in param_mapping:
+ params[param_mapping[key]] = value
+ else:
+ params[key] = value
+
+ # Special handling for shaderColorIntensity to maintain both versions
+ if "shaderColorIntensity" in loaded_params:
+ # Keep the original key
+ params["shaderColorIntensity"] = loaded_params["shaderColorIntensity"]
+ # Also provide as intensity for backward compatibility
+ params["intensity"] = loaded_params["shaderColorIntensity"]
+
+ # Handle specific shader type mapping
+ if "shader_type" in params:
+ shader_type = params["shader_type"]
+ # Convert string values to standardized format
+ if shader_type.lower() == "tensor_field" or shader_type.lower() == "tensorfield":
+ params["shader_type"] = "tensor_field"
+ elif shader_type.lower() == "heterogeneous_fbm" or shader_type.lower() == "heterogeneousfbm":
+ params["shader_type"] = "heterogeneous_fbm"
+ elif shader_type.lower() == "projection_3d" or shader_type.lower() == "projection3d":
+ params["shader_type"] = "projection_3d"
+ elif shader_type.lower() == "cellular":
+ params["shader_type"] = "cellular"
+ elif shader_type.lower() == "fractal":
+ params["shader_type"] = "fractal"
+ elif shader_type.lower() == "perlin":
+ params["shader_type"] = "perlin"
+ elif shader_type.lower() == "waves":
+ params["shader_type"] = "waves"
+ elif shader_type.lower() == "gaussian":
+ params["shader_type"] = "gaussian"
+ elif shader_type.lower() == "domain_warp":
+ params["shader_type"] = "domain_warp"
+ elif shader_type.lower() == "interference" or shader_type.lower() == "interference_patterns":
+ params["shader_type"] = "interference_patterns"
+ print(f"Mapped shader type '{shader_type}' to 'interference_patterns'")
+ elif shader_type.lower() == "spectral" or shader_type.lower() == "spectral_noise":
+ params["shader_type"] = "spectral"
+ print(f"Mapped shader type '{shader_type}' to 'spectral'")
+ elif shader_type.lower() == "projection" or shader_type.lower() == "projection_3d" or shader_type.lower() == "3d_projection":
+ params["shader_type"] = "projection_3d"
+ print(f"Mapped shaderType '{shader_type}' to 'projection_3d'")
+ elif shader_type.lower() == "curl" or shader_type.lower() == "curl_noise":
+ params["shader_type"] = "curl_noise"
+ # print(f"Mapped shaderType '{shader_type}' to 'curl_noise'") # Mapped to curl_noise
+
+ # Also check if shader type is in the shaderType field (alternate field name)
+ if "shaderType" in loaded_params and "shader_type" not in params:
+ shader_type = loaded_params["shaderType"]
+ if isinstance(shader_type, str):
+ if shader_type.lower() == "tensor_field" or shader_type.lower() == "tensorfield":
+ params["shader_type"] = "tensor_field"
+ elif shader_type.lower() == "heterogeneous_fbm" or shader_type.lower() == "heterogeneousfbm":
+ params["shader_type"] = "heterogeneous_fbm"
+ elif shader_type.lower() == "projection_3d" or shader_type.lower() == "projection3d":
+ params["shader_type"] = "projection_3d"
+ elif shader_type.lower() == "cellular":
+ params["shader_type"] = "cellular"
+ elif shader_type.lower() == "fractal":
+ params["shader_type"] = "fractal"
+ elif shader_type.lower() == "perlin":
+ params["shader_type"] = "perlin"
+ elif shader_type.lower() == "waves":
+ params["shader_type"] = "waves"
+ elif shader_type.lower() == "gaussian":
+ params["shader_type"] = "gaussian"
+ elif shader_type.lower() == "domain_warp":
+ params["shader_type"] = "domain_warp"
+ elif shader_type.lower() == "interference" or shader_type.lower() == "interference_patterns":
+ params["shader_type"] = "interference_patterns"
+ print(f"Mapped shaderType '{shader_type}' to 'interference_patterns'")
+ elif shader_type.lower() == "spectral" or shader_type.lower() == "spectral_noise":
+ params["shader_type"] = "spectral"
+ print(f"Mapped shaderType '{shader_type}' to 'spectral'")
+
+ # Fill in any missing parameters with defaults
+ for key, value in default_params.items():
+ if key not in params:
+ params[key] = value
+
+ # print(f"Successfully loaded shader parameters: {params}")
+ return params
+ else:
+ print(f"Parameters file not found at: {params_file}")
+ except Exception as e:
+ print(f"Error loading shader parameters: {e}")
+
+ print(f"Using default shader parameters")
+ return default_params
+
+ @staticmethod
+ def apply_shader_to_noise(noise, shader_params=None, influence=None):
+ """
+ Apply shader effects to the initial noise before sampling
+ Implements the Sฮฑ(N) part of Lt=Sฮฑ(N)โKฮฒ(t)
+
+ Args:
+ noise: Initial noise tensor [batch, channels, height, width]
+ shader_params: Dictionary of shader parameters (or None to load from file)
+ influence: How much to blend shader noise (0.0-1.0, None uses value from params)
+
+ Returns:
+ Modified noise tensor with same shape as input
+ """
+ if shader_params is None:
+ shader_params = ShaderParamsReader.get_shader_params()
+
+ # Extract basic parameters
+ batch, channels, height, width = noise.shape
+ device = noise.device
+
+ # Use provided influence or get from parameters
+ if influence is None:
+ influence = shader_params.get("intensity", 0.8)
+
+ # Ensure influence is a float
+ influence = float(influence)
+
+ # Skip if no influence
+ if influence <= 0.0:
+ return noise
+
+ # Extract shader parameters
+ shader_type = shader_params.get("shader_type", "tensor_field")
+ viz_type = shader_params.get("visualization_type", 3) # default to ellipses
+ scale = shader_params.get("scale", 1.0)
+ phase_shift = shader_params.get("phase_shift", 0.0)
+ warp_strength = shader_params.get("warp_strength", 0.5)
+ time = shader_params.get("time", 0.0)
+ octaves = shader_params.get("octaves", 3.0)
+ seed = shader_params.get("seed", 0)
+
+ # Extract shape mask parameters
+ shape_type = shader_params.get("shape_type", "none")
+ shape_mask_strength = shader_params.get("shapemaskstrength", 1.0)
+
+ # Debug print for shape mask parameters
+ print(f"Shape mask parameters: type={shape_type}, strength={shape_mask_strength}")
+
+ # Create coordinate grid (normalized to [-1, 1])
+ y, x = torch.meshgrid(torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij')
+
+ # Combine into coordinate tensor
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch, 1, 1, 1)
+
+ # Generate different shader patterns
+ if False: # Placeholder for any future shader types to be handled here
+ pass # Generate shader_noise for other types if needed
+ else:
+ # If the shader type was one of the removed ones or is not handled,
+ # print a message and return the original noise unchanged.
+ print(f"Shader type '{shader_type}' is not handled by apply_shader_to_noise or its simple implementation was removed. Returning original noise.")
+ return noise
+
+ # -- REMOVED Unreachable code: permutation, normalization, expansion, blending --
+
+ @staticmethod
+ def _lerp(a, b, t):
+ """Helper for linear interpolation."""
+ return a + (b - a) * t
+
+ @staticmethod
+ def _hsv_to_rgb(h, s, v):
+ """
+ Convert HSV to RGB.
+ h, s, v are expected in [0,1] range and shape [B, 1, H, W].
+ Returns R, G, B components, each as [B, 1, H, W] in [0,1] range.
+ """
+ # Ensure inputs are correctly shaped for broadcasting if they are single values
+ if not isinstance(h, torch.Tensor): h = torch.full_like(s if isinstance(s, torch.Tensor) else v, float(h)) # Fallback for s or v if h is scalar
+ if not isinstance(s, torch.Tensor): s = torch.full_like(h, float(s))
+ if not isinstance(v, torch.Tensor): v = torch.full_like(h, float(v))
+
+ c = v * s
+ h_prime = h * 6.0 # h is [0,1]
+
+ # Ensure h_prime is a tensor for fmod
+ if not isinstance(h_prime, torch.Tensor):
+ h_prime = torch.full_like(c, float(h_prime))
+
+ x = c * (1.0 - torch.abs(torch.fmod(h_prime, 2.0) - 1.0))
+ m = v - c
+
+ r, g, b = torch.zeros_like(h), torch.zeros_like(h), torch.zeros_like(h)
+
+ # Masks for hue ranges
+ mask0 = (h_prime < 1.0)
+ mask1 = (h_prime >= 1.0) & (h_prime < 2.0)
+ mask2 = (h_prime >= 2.0) & (h_prime < 3.0)
+ mask3 = (h_prime >= 3.0) & (h_prime < 4.0)
+ mask4 = (h_prime >= 4.0) & (h_prime < 5.0)
+ mask5 = (h_prime >= 5.0) # covers up to 6.0
+
+ # Assign R, G, B based on hue
+ r[mask0], g[mask0], b[mask0] = c[mask0], x[mask0], torch.zeros_like(x)[mask0]
+ r[mask1], g[mask1], b[mask1] = x[mask1], c[mask1], torch.zeros_like(x)[mask1]
+ r[mask2], g[mask2], b[mask2] = torch.zeros_like(x)[mask2], c[mask2], x[mask2]
+ r[mask3], g[mask3], b[mask3] = torch.zeros_like(x)[mask3], x[mask3], c[mask3]
+ r[mask4], g[mask4], b[mask4] = x[mask4], torch.zeros_like(x)[mask4], c[mask4]
+ r[mask5], g[mask5], b[mask5] = c[mask5], torch.zeros_like(x)[mask5], x[mask5]
+
+ r, g, b = r + m, g + m, b + m
+ return r, g, b
+
+ @staticmethod
+ def _interpolate_colors(stops, t):
+ """
+ Interpolate colors based on stops.
+ t is a normalized value tensor [B, 1, H, W] in [0,1] range.
+ stops: list of [value, color_tuple_or_tensor e.g. (R,G,B) or [1,3,1,1] tensor].
+ Returns R, G, B components, each as [B, 1, H, W] in [0,1] range (if stops are [0,1]).
+ """
+ device = t.device
+ dtype = t.dtype
+
+ processed_stops = []
+ for val, color_val in stops:
+ if isinstance(color_val, (list, tuple)):
+ c_tensor = torch.tensor(color_val, device=device, dtype=dtype).view(1, 3, 1, 1)
+ else: # assume it's already a tensor of shape [1,3,1,1] or broadcastable
+ c_tensor = color_val.to(device=device, dtype=dtype)
+ if len(c_tensor.shape) == 1 and c_tensor.shape[0] == 3: # e.g. torch.tensor([r,g,b])
+ c_tensor = c_tensor.view(1, 3, 1, 1)
+ processed_stops.append((float(val), c_tensor))
+
+ # Initialize final_color based on t's shape [B, 1, H, W] -> output [B, 3, H, W]
+ final_color = torch.zeros((t.shape[0], 3, t.shape[2], t.shape[3]), device=device, dtype=dtype)
+
+ for i in range(len(processed_stops) - 1):
+ t0, c0 = processed_stops[i] # c0 is [1, 3, 1, 1]
+ t1, c1 = processed_stops[i+1] # c1 is [1, 3, 1, 1]
+
+ # Mask for pixels in the current segment
+ if i == len(processed_stops) - 2: # Last segment includes t1
+ segment_mask = (t >= t0) & (t <= t1)
+ else: # Other segments are [t0, t1)
+ segment_mask = (t >= t0) & (t < t1)
+
+ # Normalize t within the segment [t0, t1] -> [0, 1]
+ denominator = (t1 - t0)
+ # Avoid division by zero if t0 == t1
+ safe_denominator = torch.where(torch.abs(denominator) < 1e-8, torch.sign(denominator) * 1e-8 + 1e-8 * (1.0 - torch.abs(torch.sign(denominator))), denominator)
+
+ local_t = (t - t0) / safe_denominator
+ local_t_clamped = torch.clamp(local_t, 0.0, 1.0) # Shape [B, 1, H, W]
+
+ # Lerp colors. c0, c1 are [1,3,1,1], local_t_clamped is [B,1,H,W]
+ # Resulting interp_color_segment will be [B,3,H,W]
+ interp_color_segment = ShaderParamsReader._lerp(c0, c1, local_t_clamped)
+
+ # Apply using segment_mask (expanded to [B,3,H,W])
+ final_color = torch.where(segment_mask.expand_as(interp_color_segment), interp_color_segment, final_color)
+
+ # Handle cases where t is outside the defined stops range
+ mask_below_first = t < processed_stops[0][0]
+ first_color_expanded = processed_stops[0][1].expand_as(final_color)
+ final_color = torch.where(mask_below_first.expand_as(final_color), first_color_expanded, final_color)
+
+ mask_above_last = t > processed_stops[-1][0]
+ last_color_expanded = processed_stops[-1][1].expand_as(final_color)
+ final_color = torch.where(mask_above_last.expand_as(final_color), last_color_expanded, final_color)
+
+ return final_color[:, 0:1], final_color[:, 1:2], final_color[:, 2:3]
+
+ @staticmethod
+ def apply_color_scheme(noise_tensor, shader_params=None):
+ """
+ Apply color scheme to a shader noise tensor based on shader_params
+
+ Args:
+ noise_tensor: Input noise tensor of shape [batch, channels, height, width]
+ shader_params: Dictionary of shader parameters (or None to load from file)
+
+ Returns:
+ Modified noise tensor with color scheme applied
+ """
+ if shader_params is None:
+ shader_params = ShaderParamsReader.get_shader_params()
+
+ # Get color scheme and intensity parameters
+ color_scheme = shader_params.get("colorScheme", "none")
+
+ # Try to get the color intensity with priority for shaderColorIntensity
+ color_intensity = shader_params.get("shaderColorIntensity",
+ shader_params.get("intensity", 0.8))
+
+ # Skip if no color scheme or zero intensity
+ if color_scheme == "none" or color_intensity <= 0.0:
+ print(f"Skipping color scheme application: scheme={color_scheme}, intensity={color_intensity}")
+ return noise_tensor
+
+ print(f"APPLYING COLOR SCHEME: {color_scheme} with intensity {color_intensity}")
+
+ # Extract dimensions
+ batch, channels, height, width = noise_tensor.shape
+ device = noise_tensor.device
+
+ # Create empty color tensor that we'll fill based on the scheme
+ color_tensor = torch.zeros_like(noise_tensor)
+
+ # Make sure to preserve the 4th channel if it exists
+ if channels > 3:
+ color_tensor[:, 3:] = noise_tensor[:, 3:]
+
+ # Helper function to normalize the noise to 0-1 range for colormaps
+ def normalize_to_01(tensor):
+ return (tensor - tensor.min()) / (tensor.max() - tensor.min() + 1e-8)
+
+ # Map base noise to 0-1 for color mapping
+ base_noise = normalize_to_01(noise_tensor[:, 0])
+
+ # Create a [B, 1, H, W] version of base_noise for helpers
+ t_color = base_noise.unsqueeze(1)
+
+ # Handle different color schemes
+ if color_scheme == "rgb":
+ # RGB color scheme: create three distinct channels
+ if channels >= 3:
+ # R channel - emphasize details in first latent dimension
+ color_tensor[:, 0] = noise_tensor[:, 0] * 1.5
+ # G channel - use second latent dimension with slight phase shift
+ color_tensor[:, 1] = noise_tensor[:, 1] * 1.3
+ # B channel - use third latent dimension with different scaling
+ color_tensor[:, 2] = noise_tensor[:, 2] * 0.8
+
+ elif color_scheme == "complementary":
+ # Complementary colors: create opposing patterns in different channels
+ if channels >= 3:
+ # First channel - original
+ color_tensor[:, 0] = noise_tensor[:, 0] * 1.5
+ # Second channel - inverted phase from channel 0
+ color_tensor[:, 1] = -noise_tensor[:, 0] * 0.8
+ # Third channel - different frequency
+ color_tensor[:, 2] = noise_tensor[:, 2] * 1.2
+
+ elif color_scheme == "monochrome":
+ # Monochrome: apply the same pattern to all channels with slight variations
+ if channels > 1:
+ base_channel = noise_tensor[:, 0:1].clone()
+ # Expand to all channels with slight variations in scaling
+ scales = torch.tensor([1.0, 0.95, 0.9, 0.85][:channels], device=device).view(1, -1, 1, 1)
+ color_tensor = base_channel * scales
+
+ elif color_scheme == "gradient":
+ # Gradient: create a position-based color gradient
+ if channels >= 3:
+ # Create coordinate grid for gradient
+ y_norm = torch.linspace(0, 1, height, device=device).view(1, 1, -1, 1).expand(batch, 1, -1, width)
+ x_norm = torch.linspace(0, 1, width, device=device).view(1, 1, 1, -1).expand(batch, 1, height, -1)
+
+ # R channel - horizontal gradient + noise
+ color_tensor[:, 0:1] = x_norm + noise_tensor[:, 0:1] * 0.4
+ # G channel - vertical gradient + noise
+ color_tensor[:, 1:2] = y_norm + noise_tensor[:, 1:2] * 0.4
+ # B channel - diagonal gradient + noise
+ color_tensor[:, 2:3] = (x_norm + y_norm) / 2 + noise_tensor[:, 2:3] * 0.4
+
+ elif color_scheme == "blue_red":
+ if channels >= 3:
+ # Blue to red gradient (cold to hot) using lerp
+ c0 = torch.tensor([0.0, 0.0, 1.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Blue
+ c1 = torch.tensor([1.0, 0.0, 0.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Red
+ # _lerp expects t_color to be broadcastable with c0, c1.
+ # t_color is [B,1,H,W], c0/c1 are [1,3,1,1]. Result is [B,3,H,W]
+ interpolated_color = ShaderParamsReader._lerp(c0, c1, t_color)
+ color_tensor[:, 0:1] = interpolated_color[:, 0:1] # Red
+ color_tensor[:, 1:2] = interpolated_color[:, 1:2] # Green
+ color_tensor[:, 2:3] = interpolated_color[:, 2:3] # Blue
+
+ elif color_scheme == "viridis":
+ if channels >= 3:
+ stops = [
+ (0.0, (0.267, 0.005, 0.329)), # #440154
+ (0.33, (0.188, 0.407, 0.553)), # #30678D
+ (0.66, (0.208, 0.718, 0.471)), # #35B778
+ (1.0, (0.992, 0.906, 0.143)) # #FDE724
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "plasma":
+ if channels >= 3:
+ # Use robust color stops for plasma, matching curl_noise.py
+ stops = [
+ (0.0, (0.05, 0.03, 0.53)),
+ (0.25, (0.40, 0.00, 0.66)),
+ (0.5, (0.70, 0.18, 0.53)),
+ (0.75, (0.94, 0.46, 0.25)),
+ (1.0, (0.98, 0.80, 0.08))
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "inferno":
+ if channels >= 3:
+ stops = [
+ (0.0, (0.001, 0.001, 0.016)),
+ (0.25, (0.259, 0.039, 0.408)),
+ (0.5, (0.576, 0.149, 0.404)),
+ (0.75, (0.867, 0.318, 0.227)),
+ (0.85, (0.988, 0.647, 0.039)),
+ (1.0, (0.988, 1.000, 0.643))
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "magma":
+ if channels >= 3:
+ stops = [
+ (0.0, (0.001, 0.001, 0.016)),
+ (0.25, (0.231, 0.059, 0.439)),
+ (0.5, (0.549, 0.161, 0.506)),
+ (0.75, (0.871, 0.288, 0.408)),
+ (0.85, (0.996, 0.624, 0.427)),
+ (1.0, (0.988, 0.992, 0.749))
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "turbo":
+ if channels >= 3:
+ stops = [
+ (0.0, (0.188, 0.071, 0.235)),
+ (0.25, (0.275, 0.408, 0.859)),
+ (0.5, (0.149, 0.749, 0.549)),
+ (0.65, (0.831, 1.000, 0.314)),
+ (0.85, (0.980, 0.718, 0.298)),
+ (1.0, (0.729, 0.004, 0.000))
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ # Turbo often benefits from a slight boost/rescale
+ r, g, b = r * 1.2 - 0.1, g * 1.2 - 0.1, b * 1.2 - 0.1
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = torch.clamp(r, 0, 1), torch.clamp(g, 0, 1), torch.clamp(b, 0, 1)
+
+ elif color_scheme == "jet":
+ if channels >= 3:
+ stops = [
+ (0.0, (0.000, 0.000, 0.5)), # Dark Blue
+ (0.125, (0.000, 0.000, 1.000)),# Blue
+ (0.375, (0.000, 1.000, 1.000)),# Cyan
+ (0.625, (1.000, 1.000, 0.000)),# Yellow
+ (0.875, (1.000, 0.000, 0.000)),# Red
+ (1.0, (0.500, 0.000, 0.000)) # Dark Red
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "rainbow":
+ if channels >= 3:
+ # Use HSV to RGB for rainbow: hue from t_color, constant saturation and value
+ hue = t_color # base_noise is already [B,1,H,W] and [0,1]
+ saturation = torch.ones_like(hue) * 0.9 # High saturation
+ value = torch.ones_like(hue) * 0.9 # Bright value
+ r, g, b = ShaderParamsReader._hsv_to_rgb(hue, saturation, value)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "cool":
+ if channels >= 3:
+ c0 = torch.tensor([0.0, 1.0, 1.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Cyan
+ c1 = torch.tensor([1.0, 0.0, 1.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Magenta
+ interpolated_color = ShaderParamsReader._lerp(c0, c1, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = interpolated_color[:,0:1], interpolated_color[:,1:2], interpolated_color[:,2:3]
+
+ elif color_scheme == "hot":
+ if channels >= 3:
+ stops = [
+ (0.0, (0.0, 0.0, 0.0)), # Black
+ (0.375, (1.0, 0.0, 0.0)), # Red
+ (0.75, (1.0, 1.0, 0.0)), # Yellow
+ (1.0, (1.0, 1.0, 1.0)) # White
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "parula":
+ if channels >= 3:
+ stops = [
+ (0.0, (0.208, 0.165, 0.529)), # #352a87
+ (0.25, (0.059, 0.361, 0.867)), # #0f5cdd
+ (0.5, (0.000, 0.710, 0.651)), # #00b5a6
+ (0.75, (1.000, 0.765, 0.216)), # #ffc337
+ (1.0, (0.988, 0.996, 0.643)) # #fcfea4
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "hsv":
+ if channels >= 3:
+ hue = t_color
+ saturation = torch.ones_like(hue) * 0.95 # Full saturation
+ value = torch.ones_like(hue) * 0.95 # Full value
+ r, g, b = ShaderParamsReader._hsv_to_rgb(hue, saturation, value)
+ # Original SPR HSV scaled output to [-1,1]. We keep [0,1] from _hsv_to_rgb for consistency with other interpolated.
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "autumn":
+ if channels >= 3:
+ c0 = torch.tensor([1.0, 0.0, 0.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Red
+ c1 = torch.tensor([1.0, 1.0, 0.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Yellow
+ interpolated_color = ShaderParamsReader._lerp(c0, c1, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = interpolated_color[:,0:1], interpolated_color[:,1:2], interpolated_color[:,2:3]
+
+ elif color_scheme == "winter":
+ if channels >= 3:
+ c0 = torch.tensor([0.0, 0.0, 1.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Blue
+ c1 = torch.tensor([0.0, 1.0, 0.5], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Greenish-Cyan
+ interpolated_color = ShaderParamsReader._lerp(c0, c1, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = interpolated_color[:,0:1], interpolated_color[:,1:2], interpolated_color[:,2:3]
+
+ elif color_scheme == "spring":
+ if channels >= 3:
+ c0 = torch.tensor([1.0, 0.0, 1.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Magenta
+ c1 = torch.tensor([1.0, 1.0, 0.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Yellow
+ interpolated_color = ShaderParamsReader._lerp(c0, c1, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = interpolated_color[:,0:1], interpolated_color[:,1:2], interpolated_color[:,2:3]
+
+ elif color_scheme == "summer":
+ if channels >= 3:
+ c0 = torch.tensor([0.0, 0.5, 0.4], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Dark Green
+ c1 = torch.tensor([1.0, 1.0, 0.4], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Yellow
+ interpolated_color = ShaderParamsReader._lerp(c0, c1, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = interpolated_color[:,0:1], interpolated_color[:,1:2], interpolated_color[:,2:3]
+
+ elif color_scheme == "copper":
+ if channels >= 3:
+ c0 = torch.tensor([0.0, 0.0, 0.0], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Black
+ c1 = torch.tensor([1.0, 0.6235, 0.3922], device=device, dtype=t_color.dtype).view(1, 3, 1, 1) # Copper color approx (255,159,100)
+ interpolated_color = ShaderParamsReader._lerp(c0, c1, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = interpolated_color[:,0:1], interpolated_color[:,1:2], interpolated_color[:,2:3]
+
+ elif color_scheme == "pink":
+ if channels >= 3:
+ stops = [
+ (0.0, (0.05, 0.05, 0.05)), # Dark gray
+ (0.5, (1.0, 0.41, 0.71)), # Hot Pink approx
+ (1.0, (1.0, 0.75, 0.80)) # Light Pink
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "bone":
+ if channels >= 3:
+ stops = [ # Standard bone colormap
+ (0.0, (0.0, 0.0, 0.0)),
+ (0.375, (0.3294, 0.3294, 0.4549)), # (84, 84, 116)
+ (0.75, (0.6275, 0.7569, 0.7569)), # (160, 193, 193)
+ (1.0, (1.0, 1.0, 1.0))
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ # Original shader_params_reader 'bone' scaled to [-1,1]. Let's keep [0,1] for consistency.
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "ocean":
+ if channels >= 3:
+ stops = [ # Based on matplotlib's ocean
+ (0.0, (0.0, 0.0, 0.0)), # Black
+ (0.33, (0.0, 0.0, 0.5)), # Dark Blue
+ (0.66, (0.0, 0.5, 1.0)), # Light Blue
+ (1.0, (0.7, 1.0, 1.0)) # Very Light Cyan/White
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "terrain":
+ if channels >= 3:
+ stops = [ # Standard terrain colormap
+ (0.0, (0.2, 0.2, 0.6)), # Deep water blue
+ (0.15, (0.0, 0.5, 0.0)), # Dark Green (low land)
+ (0.33, (0.0, 0.8, 0.4)), # Green (land)
+ (0.5, (0.87, 0.87, 0.4)), # Yellowish (hills)
+ (0.75, (0.6, 0.4, 0.2)), # Brown (mountains)
+ (1.0, (1.0, 1.0, 1.0)) # White (snow peaks)
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "neon":
+ if channels >= 3:
+ # Using a multi-stop lerp for vibrant neon effect
+ stops = [
+ (0.0, (1.0, 0.0, 0.5)), # Magenta
+ (0.33, (0.0, 1.0, 1.0)), # Cyan
+ (0.66, (1.0, 1.0, 0.0)), # Yellow
+ (1.0, (0.5, 0.0, 1.0)) # Purple
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "fire":
+ if channels >= 3:
+ stops = [ # Standard fire colormap
+ (0.0, (0.0, 0.0, 0.0)), # Black
+ (0.25, (1.0, 0.0, 0.0)), # Red
+ (0.6, (1.0, 1.0, 0.0)), # Yellow
+ (1.0, (1.0, 1.0, 1.0)) # White
+ ]
+ r, g, b = ShaderParamsReader._interpolate_colors(stops, t_color)
+ color_tensor[:, 0:1], color_tensor[:, 1:2], color_tensor[:, 2:3] = r, g, b
+
+ elif color_scheme == "fantasy":
+ # Fantasy colors: magical and otherworldly - keeping original SPR logic
+ if channels >= 3:
+ # Create swirling color pattern
+ angle = torch.atan2(noise_tensor[:, 1], noise_tensor[:, 0])
+ radius = torch.sqrt(noise_tensor[:, 0]**2 + noise_tensor[:, 1]**2)
+
+ # Purple/pink base
+ color_tensor[:, 0] = torch.sin(angle * 2.0 + radius * 3.0) * 0.5 + 0.5
+ # Teal/blue variations
+ color_tensor[:, 1] = torch.sin(angle * 3.0 - radius * 2.0) * 0.5 + 0.5
+ # Golden highlights
+ color_tensor[:, 2] = torch.sin(radius * 5.0) * 0.5 + 0.5
+
+ # Normalize to maintain proper distribution
+ color_tensor = (color_tensor - 0.5) * 2.0
+ else:
+ # Default case - return original noise if color scheme not implemented or recognized
+ print(f"WARNING: Color scheme '{color_scheme}' not recognized, using original noise")
+ return noise_tensor
+
+ # Blend with original based on intensity
+ # Ensure color_tensor values are appropriately scaled if necessary before blending.
+ # For now, assuming [0,1] range from most new schemes is acceptable for blending.
+ result = noise_tensor * (1.0 - color_intensity) + color_tensor * color_intensity
+ print(f"Applied {color_scheme} color scheme - result shape: {result.shape}")
+ return result
+
+ @staticmethod
+ def apply_shape_mask(coords_normalized_01, shape_type, time=0.0, base_seed=0, use_temporal_coherence=False):
+ """
+ Apply shape mask to coordinates.
+ Coordinates are expected to be in the [0, 1] range.
+
+ Args:
+ coords_normalized_01: Coordinate tensor [batch, height, width, 2] in [0, 1] range.
+ shape_type: Type of shape to apply (integer or string).
+ time: Animation time.
+ base_seed: Base seed for randomness if shapes require it.
+ use_temporal_coherence: Flag for temporal coherence.
+
+ Returns:
+ Shape mask tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = coords_normalized_01.shape
+ device = coords_normalized_01.device
+
+ # For shapes that assume coordinates centered at (0,0) and range approx [-0.5, 0.5] or [-1,1]
+ # we create centered coordinates from the [0,1] input.
+ centered_coords = coords_normalized_01 - 0.5 # Now in [-0.5, 0.5] range
+
+ # Distance from center for centered_coords
+ center_dist = torch.sqrt(centered_coords[:, :, :, 0]**2 + centered_coords[:, :, :, 1]**2) # Max dist ~0.707
+
+ # Angle from center for centered_coords
+ angle = torch.atan2(centered_coords[:, :, :, 1], centered_coords[:, :, :, 0])
+
+ # Default mask
+ mask_output = torch.ones((batch, height, width), device=device)
+
+ # Convert string shape_type to standardized string format
+ if isinstance(shape_type, str):
+ shape_type = shape_type.lower()
+
+ # Handle both numeric and string shape types
+ # Note: Shapes from original apply_shape_mask are adapted to the new coordinate system.
+ # The radius/size parameters might need adjustment if they were tuned for [-1,1] p.
+
+ if shape_type == 1 or shape_type == "circle": # Original "radial" was also circle
+ # Circle - centered_coords range from approx -0.5 to 0.5. center_dist max ~0.707
+ # To make a circle that fills most of the [0,1] original space, radius should be ~0.5
+ # CN version: 1.0 - torch.clamp(dist * 2, 0, 1) where dist is from center of [0,1] grid
+ # For coords_normalized_01, dist from center (0.5,0.5) is `center_dist_01`
+ center_x_01, center_y_01 = 0.5, 0.5
+ y_diff_01 = coords_normalized_01[:, :, :, 1] - center_y_01
+ x_diff_01 = coords_normalized_01[:, :, :, 0] - center_x_01
+ dist_01 = torch.sqrt(x_diff_01**2 + y_diff_01**2)
+ mask_output = 1.0 - torch.clamp(dist_01 * 2.0, 0.0, 1.0) # Match CN circle
+
+ elif shape_type == 2 or shape_type == "square":
+ # Square - centered_coords values are in [-0.5, 0.5]
+ # CN version: x_mask = torch.abs(coords_bhwc[:, :, :, 0] - 0.5) * 2
+ # y_mask = torch.abs(coords_bhwc[:, :, :, 1] - 0.5) * 2
+ # dist = torch.max(x_mask, y_mask)
+ # shape_mask = 1.0 - torch.clamp(dist, 0, 1)
+ # This uses coords_normalized_01 (same as coords_bhwc in CN)
+ x_mask_sq = torch.abs(coords_normalized_01[:, :, :, 0] - 0.5) * 2.0
+ y_mask_sq = torch.abs(coords_normalized_01[:, :, :, 1] - 0.5) * 2.0
+ dist_sq = torch.max(x_mask_sq, y_mask_sq)
+ mask_output = 1.0 - torch.clamp(dist_sq, 0.0, 1.0)
+
+ elif shape_type == "radial": # Use the same logic as radial_animated (curl_noise.py radial)
+ # Uses coords_normalized_01 (range [0,1])
+ time_tensor = torch.tensor(time, device=device, dtype=coords_normalized_01.dtype)
+ center_x = 0.5 + 0.2 * torch.cos(time_tensor)
+ center_y = 0.5 + 0.2 * torch.sin(time_tensor)
+ # Calculate distance from the animated center using coords_normalized_01
+ y_diff = coords_normalized_01[:, :, :, 1] - center_y
+ x_diff = coords_normalized_01[:, :, :, 0] - center_x
+ dist_from_anim_center = torch.sqrt(x_diff**2 + y_diff**2) * 2.0 # Multiplied by 2 like in curl_noise
+ mask_output = torch.clamp(1.0 - dist_from_anim_center, 0.0, 1.0)
+
+ elif shape_type == 3 or shape_type == "star": # SPR original "star"
+ # Star-like shape - using centered_coords
+ points = 5.0
+ star_radius = 0.25 + 0.125 * torch.cos(angle * points)
+ mask_output = (center_dist < star_radius).float()
+
+ elif shape_type == "linear": # Ported from curl_noise.py
+ # Uses original [0,1] coordinates (coords_normalized_01)
+ # Convert time calculation to tensor for torch.fmod
+ time_tensor_02 = torch.tensor(time * 0.2, device=device, dtype=coords_normalized_01.dtype)
+ x_offset = torch.fmod(time_tensor_02, 1.0) * 2.0 # Match JS fract animation
+ shifted_x = torch.fmod(coords_normalized_01[:, :, :, 0] + x_offset, 1.0)
+ mask_output = shifted_x # Mask values will be [0,1]
+
+ elif shape_type == "radial_animated": # Ported from curl_noise.py (its "radial" shape)
+ # Uses coords_normalized_01 (range [0,1])
+ time_tensor = torch.tensor(time, device=device, dtype=coords_normalized_01.dtype)
+ center_x = 0.5 + 0.2 * torch.cos(time_tensor)
+ center_y = 0.5 + 0.2 * torch.sin(time_tensor)
+ # Calculate distance from the animated center using coords_normalized_01
+ y_diff = coords_normalized_01[:, :, :, 1] - center_y
+ x_diff = coords_normalized_01[:, :, :, 0] - center_x
+ dist_from_anim_center = torch.sqrt(x_diff**2 + y_diff**2) * 2.0 # Multiplied by 2 like in curl_noise
+ mask_output = torch.clamp(1.0 - dist_from_anim_center, 0.0, 1.0)
+
+ elif shape_type == "spiral": # Ported and enhanced from curl_noise.py, uses centered_coords
+ # centered_coords are in [-0.5, 0.5]
+ # theta and r are calculated from centered_coords
+ # angle = atan2(centered_coords_y, centered_coords_x) - already available as 'angle'
+ # r = norm(centered_coords) * 2.0 - center_dist is norm(centered_coords), so r = center_dist * 2.0
+ r_spiral = center_dist * 2.0 # center_dist is norm of coords in [-0.5,0.5], max ~0.707. So r_spiral max ~1.414
+
+ time_tensor = torch.tensor(time, device=device, dtype=coords_normalized_01.dtype)
+ theta_animated = angle + time_tensor # angle is already calculated from centered_coords
+
+ mask_output = torch.fmod((theta_animated / (2.0 * math.pi) + r_spiral), 1.0)
+
+ elif shape_type == "checkerboard": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (range [0,1])
+ grid_size = 8.0 # From curl_noise.py
+ # Convert time expressions to tensors
+ time_tensor_gs_02 = torch.tensor(time * grid_size * 0.2, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_gs_01 = torch.tensor(time * grid_size * 0.1, device=device, dtype=coords_normalized_01.dtype)
+
+ x_offset = time_tensor_gs_02
+ y_offset = time_tensor_gs_01
+
+ # Apply offset and scale for grid calculation
+ x_grid_val = (coords_normalized_01[:, :, :, 0] + x_offset / grid_size) * grid_size
+ y_grid_val = (coords_normalized_01[:, :, :, 1] + y_offset / grid_size) * grid_size
+
+ # Floor and compute pattern. The *0.5 and fmod 1.0 results in 0 or 0.5 values, then combined for 0, 0.5, 1.0.
+ # To get a binary mask (0 or 1), we can fmod the sum by 2 and then check if it's < 1, or directly use fmod 1.0 from curl_noise.
+ x_grid_processed = torch.floor(x_grid_val) * 0.5
+ y_grid_processed = torch.floor(y_grid_val) * 0.5
+ mask_output = torch.fmod(x_grid_processed + y_grid_processed, 1.0)
+
+ elif shape_type == "spots": # Ported from curl_noise.py (more complex version)
+ # Uses coords_normalized_01 (same as coords_bhwc in CN)
+ mask_spots_cn = torch.zeros_like(coords_normalized_01[:, :, :, 0])
+ num_spots_cn = 10
+ time_tensor_cn_spots = torch.tensor(time, device=device, dtype=coords_normalized_01.dtype)
+
+ for i in range(num_spots_cn):
+ # Use ShaderParamsReader.random_val
+ rand_x_cn = ShaderParamsReader.random_val(coords_normalized_01, base_seed, i * 78)
+ rand_y_cn = ShaderParamsReader.random_val(coords_normalized_01, base_seed, i * 12)
+ size_cn_base = (ShaderParamsReader.random_val(coords_normalized_01, base_seed, i * 93) * 0.3 + 0.1)
+
+ angle_float_cn = time + float(i) # time is already a float or tensor
+ angle_tensor_cn = torch.tensor(angle_float_cn, device=device, dtype=coords_normalized_01.dtype)
+ spot_pos_x_cn = 0.5 + torch.cos(angle_tensor_cn) * 0.4 * rand_x_cn
+ spot_pos_y_cn = 0.5 + torch.sin(angle_tensor_cn) * 0.4 * rand_y_cn
+
+ size_anim_angle_cn = torch.tensor(time * 2.0 + float(i), device=device, dtype=coords_normalized_01.dtype)
+ size_cn_final = size_cn_base * (1.0 + 0.2 * torch.sin(size_anim_angle_cn))
+
+ dist_cn_spots = torch.sqrt((coords_normalized_01[:, :, :, 0] - spot_pos_x_cn)**2 + (coords_normalized_01[:, :, :, 1] - spot_pos_y_cn)**2)
+ # Avoid division by zero or very small size
+ spot_mask_cn_indiv = torch.clamp(1.0 - dist_cn_spots / (size_cn_final + 1e-8), 0.0, 1.0)
+ mask_spots_cn = torch.maximum(mask_spots_cn, spot_mask_cn_indiv)
+ mask_output = mask_spots_cn
+
+ elif shape_type == "hexgrid": # Adapted from original, using centered_coords
+ # This was complex. Let's simplify for [0,1] input.
+ # Using coords_normalized_01 directly for hexgrid based on curl_noise's hexgrid logic
+ hex_uv = coords_normalized_01 * 6.0 # Scale for hex grid density
+
+ # Convert time expressions to tensors before sin/cos
+ time_tensor_05 = torch.tensor(time * 0.5, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_03 = torch.tensor(time * 0.3, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor = torch.tensor(time, device=device, dtype=coords_normalized_01.dtype)
+
+ hex_uv_anim = hex_uv.clone() # Avoid in-place modification if hex_uv is reused
+ hex_uv_anim[:, :, :, 0] += torch.sin(time_tensor_05) * 0.5
+ hex_uv_anim[:, :, :, 1] += torch.cos(time_tensor_03) * 0.5
+
+ r_vec = torch.tensor([1.0, 1.73], device=device, dtype=coords_normalized_01.dtype).reshape(1, 1, 1, 2)
+ h_vec = r_vec * 0.5
+ a_vec = torch.fmod(hex_uv_anim, r_vec) - h_vec
+ b_vec = torch.fmod(hex_uv_anim + h_vec, r_vec) - h_vec
+
+ dist_hex = torch.minimum(torch.norm(a_vec, dim=-1), torch.norm(b_vec, dim=-1))
+ cell_size = 0.3 + 0.1 * torch.sin(time_tensor)
+ # Use ShaderParamsReader.smoothstep
+ mask_output = ShaderParamsReader.smoothstep(cell_size + 0.05, cell_size - 0.05, dist_hex)
+
+ elif shape_type == "stripes": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (range [0,1])
+ freq = 10.0
+ # Convert time expressions to tensors
+ time_tensor = torch.tensor(time, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_02 = torch.tensor(time * 0.2, device=device, dtype=coords_normalized_01.dtype)
+
+ angle_anim = 0.5 * torch.sin(time_tensor_02)
+ cos_a = torch.cos(angle_anim)
+ sin_a = torch.sin(angle_anim)
+
+ # Rotate coordinates directly using coords_normalized_01 to match curl_noise.py
+ # This rotates around the (0,0) origin of the [0,1] coordinate system.
+ rotated_x = coords_normalized_01[:, :, :, 0] * cos_a - coords_normalized_01[:, :, :, 1] * sin_a
+ # rotated_y is not strictly needed for this pattern as stripes are based on rotated_x
+
+ stripes_val = torch.sin(rotated_x * freq + time_tensor)
+ # Apply smoothstep to create distinct stripes
+ mask_output = ShaderParamsReader.smoothstep(0.0, 0.1, stripes_val) * ShaderParamsReader.smoothstep(0.0, -0.1, -stripes_val)
+
+ elif shape_type == "radial_gradient_static": # Renamed from "gradient"
+ # This is the original radial gradient from shader_params_reader.py
+ # center_dist max ~0.707 for centered_coords (which are coords_normalized_01 - 0.5).
+ # To have gradient from center to edge of original [0,1] box, we need to normalize center_dist.
+ # Max distance from center of a [0,1] box is sqrt(0.5^2+0.5^2) = ~0.707.
+ # So center_dist / 0.707 normalizes it roughly to [0,1] for points within the box.
+ # Or simpler, just use center_dist directly, it gives a gradient from 0 to ~0.7
+ mask_output = 1.0 - torch.clamp(center_dist / 0.5, 0.0, 1.0) # Soft radial gradient, 0.5 radius
+
+ elif shape_type == "gradient": # Ported from curl_noise.py (animated directional gradient)
+ # Uses coords_normalized_01 (equivalent to coords_bhwc in curl_noise.py)
+ time_tensor_02_grad = torch.tensor(time * 0.2, device=device, dtype=coords_normalized_01.dtype)
+ angle_grad = time_tensor_02_grad
+ dir_x_grad = torch.cos(angle_grad)
+ dir_y_grad = torch.sin(angle_grad)
+ # Project centered coordinates onto the direction vector
+ # coords_normalized_01 are [0,1], so (coords_normalized_01 - 0.5) makes them [-0.5, 0.5]
+ proj_grad = (coords_normalized_01[:, :, :, 0] - 0.5) * dir_x_grad + \
+ (coords_normalized_01[:, :, :, 1] - 0.5) * dir_y_grad + 0.5
+ mask_output = proj_grad # Result is roughly in [0,1] range
+
+ elif shape_type == "vignette": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (range [0,1])
+ # Convert time expressions to tensors
+ time_tensor_03 = torch.tensor(time * 0.3, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_04 = torch.tensor(time * 0.4, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_05 = torch.tensor(time * 0.5, device=device, dtype=coords_normalized_01.dtype)
+
+ center_x_anim = 0.5 + 0.2 * torch.sin(time_tensor_03)
+ center_y_anim = 0.5 + 0.2 * torch.cos(time_tensor_04)
+
+ # Calculate distance from animated center using coords_normalized_01
+ dist_from_anim_center_x = coords_normalized_01[:, :, :, 0] - center_x_anim
+ dist_from_anim_center_y = coords_normalized_01[:, :, :, 1] - center_y_anim
+ dist_vignette = torch.sqrt(dist_from_anim_center_x**2 + dist_from_anim_center_y**2)
+
+ radius_anim = 0.6 + 0.2 * torch.sin(time_tensor_05)
+ smoothness = 0.3 # As in curl_noise.py
+
+ mask_output = 1.0 - ShaderParamsReader.smoothstep(radius_anim - smoothness, radius_anim, dist_vignette)
+
+ elif shape_type == "cross": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (range [0,1]), but calculations are around center (0.5,0.5)
+ # Convert time expressions to tensors
+ time_tensor = torch.tensor(time, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_02 = torch.tensor(time * 0.2, device=device, dtype=coords_normalized_01.dtype)
+
+ thickness_anim = 0.1 + 0.05 * torch.sin(time_tensor)
+ rotation_anim = time_tensor_02
+ cos_r = torch.cos(rotation_anim)
+ sin_r = torch.sin(rotation_anim)
+
+ # Use centered_coords for rotation calculation
+ # centered_coords are already coords_normalized_01 - 0.5
+ rotated_x = centered_coords[:, :, :, 0] * cos_r - centered_coords[:, :, :, 1] * sin_r
+ rotated_y = centered_coords[:, :, :, 0] * sin_r + centered_coords[:, :, :, 1] * cos_r
+
+ # Shift back to [0,1]-like domain for comparison if needed, or compare in [-0.5,0.5] domain
+ # The original curl_noise compared rotated_x/y against 0.5 after adding 0.5.
+ # Here, rotated_x/y are already centered around 0. So, we compare against 0.
+
+ # Horizontal bar (rotated)
+ h_bar = ShaderParamsReader.smoothstep(0.0 - thickness_anim, 0.0 - thickness_anim + 0.02, rotated_y) * \
+ ShaderParamsReader.smoothstep(0.0 + thickness_anim, 0.0 + thickness_anim - 0.02, rotated_y)
+ # Vertical bar (rotated)
+ v_bar = ShaderParamsReader.smoothstep(0.0 - thickness_anim, 0.0 - thickness_anim + 0.02, rotated_x) * \
+ ShaderParamsReader.smoothstep(0.0 + thickness_anim, 0.0 + thickness_anim - 0.02, rotated_x)
+ mask_output = torch.maximum(h_bar, v_bar)
+
+ elif shape_type == "triangles": # Adapted from original, using centered_coords
+ # Re-evaluate scaling for centered_coords.
+ # Let's use coords_normalized_01 for a direct port attempt of a triangle grid like curl_noise
+ t_tensor = torch.tensor(time * 0.2, device=device, dtype=coords_normalized_01.dtype)
+ t_sin_arg = t_tensor
+ t_cos_arg = torch.tensor(time * 0.7, device=device, dtype=coords_normalized_01.dtype)
+ t_border_arg = torch.tensor(time * 1.5, device=device, dtype=coords_normalized_01.dtype)
+ scale_factor = 5.0
+ uv_tri = coords_normalized_01 * scale_factor
+ uv_tri_anim = uv_tri.clone()
+ uv_tri_anim[:, :, :, 0] += torch.sin(t_sin_arg) * 0.5
+ uv_tri_anim[:, :, :, 1] += torch.cos(t_cos_arg) * 0.5
+
+ gv = torch.fmod(uv_tri_anim, 1.0) - 0.5 # gv is now in [-0.5, 0.5]
+
+ d1 = torch.abs(gv[:, :, :, 0] + gv[:, :, :, 1])
+ d2 = torch.abs(gv[:, :, :, 0] - gv[:, :, :, 1])
+ d3 = torch.abs(gv[:, :, :, 0]) * 0.866 + torch.abs(gv[:, :, :, 1]) * 0.5 # Approx dist for equilateral
+
+ d_tri = torch.minimum(torch.minimum(d1, d2), d3) * 0.7
+
+ border_width = 0.05 + 0.03 * torch.sin(t_border_arg)
+ mask_output = ShaderParamsReader.smoothstep(border_width, border_width - 0.02, d_tri)
+
+ elif shape_type == "concentric": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (range [0,1]) for calculating distance from an animated center.
+ # Convert time expressions to tensors
+ time_tensor_03 = torch.tensor(time * 0.3, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_04 = torch.tensor(time * 0.4, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_01 = torch.tensor(time * 0.1, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_05 = torch.tensor(time * 0.5, device=device, dtype=coords_normalized_01.dtype)
+
+ center_x_anim = 0.5 + 0.2 * torch.sin(time_tensor_03)
+ center_y_anim = 0.5 + 0.2 * torch.cos(time_tensor_04)
+
+ dist_from_center = torch.sqrt((coords_normalized_01[:, :, :, 0] - center_x_anim)**2 +
+ (coords_normalized_01[:, :, :, 1] - center_y_anim)**2)
+
+ freq_anim = 10.0 + 5.0 * torch.sin(time_tensor_01)
+ phase_anim = time_tensor_05
+ rings_val = torch.sin(dist_from_center * freq_anim + phase_anim)
+ mask_output = ShaderParamsReader.smoothstep(0.0, 0.1, rings_val) * ShaderParamsReader.smoothstep(0.0, -0.1, -rings_val)
+
+ elif shape_type == "rays": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (range [0,1]) for calculating angle and dist from an animated center.
+ # Convert time expressions to tensors
+ time_tensor_03 = torch.tensor(time * 0.3, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_04 = torch.tensor(time * 0.4, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_05 = torch.tensor(time * 0.5, device=device, dtype=coords_normalized_01.dtype)
+
+ center_x_anim = 0.5 + 0.1 * torch.sin(time_tensor_03)
+ center_y_anim = 0.5 + 0.1 * torch.cos(time_tensor_04)
+
+ to_center_x = coords_normalized_01[:, :, :, 0] - center_x_anim
+ to_center_y = coords_normalized_01[:, :, :, 1] - center_y_anim
+
+ angle_rays = torch.atan2(to_center_y, to_center_x)
+ freq_rays = 8.0
+ phase_rays = time_tensor_05
+ rays_val = torch.sin(angle_rays * freq_rays + phase_rays)
+
+ dist_rays = torch.sqrt(to_center_x**2 + to_center_y**2)
+ falloff = 1.0 - ShaderParamsReader.smoothstep(0.0, 0.8, dist_rays)
+ mask_output = ShaderParamsReader.smoothstep(0.0, 0.3, rays_val) * falloff
+
+ elif shape_type == "zigzag": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (range [0,1]) and centers for rotation.
+ freq_zigzag = 10.0
+ # Convert time expressions to tensors
+ time_tensor = torch.tensor(time, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_02 = torch.tensor(time * 0.2, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_05 = torch.tensor(time * 0.5, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_03 = torch.tensor(time * 0.3, device=device, dtype=coords_normalized_01.dtype)
+
+ angle_zigzag = 0.5 * torch.sin(time_tensor_02)
+ cos_a = torch.cos(angle_zigzag)
+ sin_a = torch.sin(angle_zigzag)
+
+ # Rotate coords_normalized_01 directly to match curl_noise.py (rotation around 0,0 of the [0,1] grid)
+ rotated_x_norm = coords_normalized_01[:, :, :, 0] * cos_a - coords_normalized_01[:, :, :, 1] * sin_a
+ rotated_y_norm = coords_normalized_01[:, :, :, 0] * sin_a + coords_normalized_01[:, :, :, 1] * cos_a
+
+ # The original curl_noise performed fmod(rotated_coord * freq - time_offset, 1.0).
+ # Then 2.0 * fmod_result - 1.0 to bring to [-1,1], then abs for [0,1].
+ # Since rotated_x_norm and rotated_y_norm are in a range determined by the rotation of [0,1] coordinates,
+ # multiplying by freq_zigzag will expand this range before fmod.
+ zigzag1 = torch.abs(2.0 * torch.fmod(rotated_x_norm * freq_zigzag - time_tensor_05, 1.0) - 1.0)
+ zigzag2 = torch.abs(2.0 * torch.fmod(rotated_y_norm * freq_zigzag + time_tensor_03, 1.0) - 1.0)
+
+ zigzag_combined = torch.minimum(zigzag1, zigzag2)
+ thickness_anim = 0.3 + 0.1 * torch.sin(time_tensor)
+ # torch.heaviside(input, values) outputs values where input > 0, and 0 where input < 0.
+ # For input == 0, it outputs values[0] if it's a tensor, or just values if scalar.
+ # A common way to get a step is (input > threshold).float()
+ # curl_noise's step(edge, x) is (x >= edge).float()
+ # So, heaviside(zigzag - thickness, torch.tensor(0.5)) is similar to (zigzag - thickness >= 0).float()
+ # which is (zigzag >= thickness).float()
+ mask_output = (zigzag_combined >= thickness_anim).float() # More direct step function
+
+ elif shape_type == "gradient_x": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (range [0,1])
+ mask_output = coords_normalized_01[:, :, :, 0]
+
+ elif shape_type == "gradient_y": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (range [0,1])
+ mask_output = coords_normalized_01[:, :, :, 1]
+
+ elif shape_type == "stars": # Ported from curl_noise.py
+ # Uses coords_normalized_01 (same as coords_bhwc in CN)
+ mask_stars_cn = torch.zeros_like(coords_normalized_01[:, :, :, 0])
+ num_stars_cn = 20
+ time_tensor_cn_stars = torch.tensor(time, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_01_cn = torch.tensor(time * 0.1, device=device, dtype=coords_normalized_01.dtype)
+ time_tensor_015_cn = torch.tensor(time * 0.15, device=device, dtype=coords_normalized_01.dtype)
+
+ for i in range(num_stars_cn):
+ # Use ShaderParamsReader.random_val
+ rand_x_stars = ShaderParamsReader.random_val(coords_normalized_01, base_seed, i * 78 + 10)
+ rand_y_stars = ShaderParamsReader.random_val(coords_normalized_01, base_seed, i * 12 + 20)
+
+ time_sin_arg_stars = torch.tensor(float(i), device=device, dtype=coords_normalized_01.dtype) + time_tensor_01_cn
+ time_cos_arg_stars = torch.tensor(float(i) * 1.5, device=device, dtype=coords_normalized_01.dtype) + time_tensor_015_cn
+
+ star_pos_x_cn = torch.fmod(rand_x_stars + 0.05 * torch.sin(time_sin_arg_stars), 1.0)
+ star_pos_y_cn = torch.fmod(rand_y_stars + 0.05 * torch.cos(time_cos_arg_stars), 1.0)
+
+ brightness_arg_stars = torch.tensor(float(i), device=device, dtype=coords_normalized_01.dtype) + time_tensor_cn_stars * (0.5 + rand_x_stars * 0.5)
+ brightness_cn = 0.5 + 0.5 * torch.sin(brightness_arg_stars)
+ size_stars_cn = 0.01 + 0.015 * rand_y_stars * brightness_cn
+
+ dist_stars_cn = torch.sqrt((coords_normalized_01[:, :, :, 0] - star_pos_x_cn)**2 + (coords_normalized_01[:, :, :, 1] - star_pos_y_cn)**2)
+ # Use ShaderParamsReader.smoothstep for soft stars
+ star_mask_cn_indiv = ShaderParamsReader.smoothstep(size_stars_cn, size_stars_cn * 0.5, dist_stars_cn) * brightness_cn
+ mask_stars_cn = torch.maximum(mask_stars_cn, star_mask_cn_indiv)
+ mask_output = mask_stars_cn
+
+ else: # Default for unknown or "none"
+ if shape_type not in ["none", "0", 0]: # Only print warning for actual unknown types
+ print(f"Unknown shape type: {shape_type}, using default (full mask)")
+ # Default is full mask (all ones)
+ mask_output = torch.ones((batch, height, width), device=device)
+
+ # Ensure mask_output is [B, H, W, 1]
+ if len(mask_output.shape) == 3: # If it's [B, H, W]
+ mask_output = mask_output.unsqueeze(-1)
+ elif len(mask_output.shape) == 4 and mask_output.shape[-1] != 1: # If it's [B,H,W,C] C!=1
+ print(f"Warning: Shape mask generated with {mask_output.shape[-1]} channels. Taking first channel.")
+ mask_output = mask_output[..., 0:1]
+
+ return mask_output
+
+# Legacy functions for backward compatibility
+def get_shader_params():
+ """Legacy function that calls the new class method"""
+ return ShaderParamsReader.get_shader_params()
+
+def test_params():
+ """Test function to check if parameters are loading correctly"""
+ params = ShaderParamsReader.get_shader_params()
+ print(f"TEST: Current shader parameters: {params}")
+ return params
+
+def generate_noise_tensor(shader_params, height, width, batch_size=1, device="cuda", seed=0, target_channels=None):
+ """
+ Legacy function that uses the new class to generate noise
+
+ Args:
+ shader_params: Dictionary containing shader parameters
+ height: Height of the tensor
+ width: Width of the tensor
+ batch_size: Number of images in the batch
+ device: Device to create tensor on
+ seed: Random seed for deterministic noise generation
+ target_channels: Number of output channels (optional, default is 4)
+
+ Returns:
+ Noise tensor with shape [batch_size, channels, height, width]
+ where channels is determined by target_channels (default: 4)
+ """
+ # Import ShaderToTensor for direct shader noise generation
+ from .shader_to_tensor import ShaderToTensor
+
+ # Make a copy of shader_params to avoid modifying the original
+ shader_params = shader_params.copy()
+
+ # Add target_channels to shader_params if provided
+ if target_channels is not None:
+ shader_params["target_channels"] = target_channels
+ print(f"Using target_channels={target_channels} in default generator")
+
+ # Check if we're using specialized shader types which are handled separately in the sampler
+ shader_type = shader_params.get("shader_type", "tensor_field")
+ if shader_type == "cellular":
+ print("Note: Cellular shader type detected in generate_noise_tensor, but this will be handled by the specialized cellular noise generator")
+ elif shader_type == "fractal":
+ print("Note: Fractal shader type detected in generate_noise_tensor, but this will be handled by the specialized fractal noise generator")
+ elif shader_type == "perlin":
+ print("Note: Perlin shader type detected in generate_noise_tensor, but this will be handled by the specialized perlin noise generator")
+ elif shader_type == "waves":
+ print("Note: Waves shader type detected in generate_noise_tensor, but this will be handled by the specialized waves noise generator")
+ elif shader_type == "gaussian":
+ print("Note: Gaussian shader type detected in generate_noise_tensor, but this will be handled by the specialized gaussian noise generator")
+ elif shader_type == "tensor_field":
+ print("Note: Tensor field shader type detected in generate_noise_tensor, but this will be handled by the specialized tensor field generator")
+ elif shader_type == "heterogeneous_fbm":
+ print("Note: Heterogeneous FBM shader type detected in generate_noise_tensor, but this will be handled by the specialized generator")
+ elif shader_type == "interference_patterns":
+ print("Note: Interference patterns shader type detected in generate_noise_tensor, but this will be handled by the specialized generator")
+
+ # Use the seed for deterministic generation
+ torch.manual_seed(seed)
+
+ # Extract parameters from shader_params
+ viz_type = shader_params.get("visualization_type", 3)
+ scale = shader_params.get("scale", 1.0)
+ warp_strength = shader_params.get("warp_strength", 0.5)
+ phase_shift = shader_params.get("phase_shift", 0.0)
+ time = shader_params.get("time", 0.0)
+ octaves = shader_params.get("octaves", 3.0)
+ shape_type = shader_params.get("shape_type", "none")
+ shape_mask_strength = shader_params.get("shapemaskstrength", 1.0)
+
+ # Generate shader noise directly using ShaderToTensor class
+ # This avoids using random noise as a starting point
+ shader_noise = ShaderToTensor.shader_noise_to_tensor(
+ batch_size=batch_size,
+ height=height,
+ width=width,
+ shader_type=shader_type,
+ visualization_type=viz_type,
+ scale=scale,
+ phase_shift=phase_shift,
+ warp_strength=warp_strength,
+ time=time,
+ device=device,
+ seed=seed,
+ octaves=octaves,
+ shape_type=shape_type,
+ shape_mask_strength=shape_mask_strength,
+ shader_params=shader_params
+ )
+
+ # Reset random seed state
+ torch.manual_seed(torch.seed())
+
+ # Apply color scheme transformation
+ color_scheme = shader_params.get("colorScheme", "none")
+ color_intensity = shader_params.get("shaderColorIntensity", 0.8)
+
+ if color_scheme != "none" and color_intensity > 0:
+ print(f"Applying color scheme: {color_scheme} with intensity: {color_intensity}")
+
+ # Track channel stats before applying color
+ num_channels = shader_noise.shape[1]
+ channel_means_before = [shader_noise[:, i].mean().item() for i in range(num_channels)]
+ channel_stds_before = [shader_noise[:, i].std().item() for i in range(num_channels)]
+ print(f"Channel means before color: {[f'{m:.4f}' for m in channel_means_before]}")
+
+ # Apply color scheme
+ colored_noise = ShaderParamsReader.apply_color_scheme(shader_noise, shader_params)
+
+ # Track channel stats after applying color
+ channel_means_after = [colored_noise[:, i].mean().item() for i in range(num_channels)]
+ channel_stds_after = [colored_noise[:, i].std().item() for i in range(num_channels)]
+ print(f"Channel means after color: {[f'{m:.4f}' for m in channel_means_after]}")
+
+ # Normalize each channel separately while preserving mean differences
+ # This ensures the color impact remains visible
+ # We only normalize the standard deviation to keep it at ~1.0
+ normalized_colored_noise = torch.zeros_like(colored_noise)
+ for i in range(num_channels):
+ # Only normalize the standard deviation while keeping the mean offset
+ channel = colored_noise[:, i:i+1]
+ normalized_colored_noise[:, i:i+1] = (channel - channel.mean()) / (channel.std() + 1e-8) + channel_means_after[i]
+
+ # Verify the normalization preserved color differences
+ final_means = [normalized_colored_noise[:, i].mean().item() for i in range(num_channels)]
+ final_stds = [normalized_colored_noise[:, i].std().item() for i in range(num_channels)]
+ print(f"Final means after normalization: {[f'{m:.4f}' for m in final_means]}")
+ print(f"Final stds after normalization: {[f'{s:.4f}' for s in final_stds]}")
+
+ return normalized_colored_noise
+
+ return shader_noise
+
+# Example of use in sampling process:
+#
+# 1. Load shader parameters
+# shader_params = ShaderParamsReader.get_shader_params()
+#
+# 2. Generate initial noise for sampling
+# noise = comfy.sample.prepare_noise(latent_samples, seed, batch_inds)
+#
+# 3. Apply shader transformation to noise (implements Sฮฑ(N))
+# modified_noise = ShaderParamsReader.apply_shader_to_noise(noise, shader_params)
+#
+# 4. Use modified noise in sampling (implements Kฮฒ(t))
+# samples = comfy.sample.sample(
+# model=model,
+# noise=modified_noise,
+# # ... other parameters
+# disable_noise=True, # Using our pre-modified noise
+# )
\ No newline at end of file
diff --git a/shader_to_tensor.py b/shader_to_tensor.py
new file mode 100644
index 0000000..cd36aaf
--- /dev/null
+++ b/shader_to_tensor.py
@@ -0,0 +1,609 @@
+import torch
+import math
+
+class ShaderToTensor:
+ """
+ Class for converting shader operations to PyTorch tensors
+ This class serves as the bridge between WebGL shaders and PyTorch tensor operations
+
+ Methods are designed to closely match the shader implementations in the web UI
+ """
+
+ @classmethod
+ def shader_noise_to_tensor(cls, batch_size=1, height=64, width=64, shader_type="tensor_field",
+ visualization_type=3, scale=1.0, phase_shift=0.0,
+ warp_strength=0.0, time=0.0, device="cuda", seed=0, octaves=3.0,
+ shape_type="none", shape_mask_strength=1.0, shader_params=None):
+ """
+ Generate noise tensors from shader parameters
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_type: Type of shader ("tensor_field", "heterogeneous_fbm", "projection_3d")
+ visualization_type: Type of visualization for tensor field (0-3)
+ scale: Scale factor
+ phase_shift: Contrast adjustment
+ warp_strength: Warp amount
+ time: Animation time
+ device: Device to create tensor on
+ seed: Random seed
+ octaves: Number of octaves for fractal noise
+ shape_type: Type of shape mask to apply
+ shape_mask_strength: Strength of shape mask application
+ shader_params: Full shader parameters dictionary (if provided)
+
+ Returns:
+ Tensor with shape [batch_size, 4, height, width]
+ """
+ # If shader_params is provided, use the dedicated method instead
+ if shader_params is not None:
+ return cls.shader_noise_to_tensor_with_params(
+ batch_size, height, width, shader_type,
+ shader_params, time, device, seed
+ )
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Generate different shader patterns
+ if shader_type == "tensor_field":
+ # Simple tensor field pattern
+ shader_noise = cls.tensor_field(
+ p, visualization_type, scale, warp_strength, phase_shift, time, device
+ )
+ elif shader_type == "heterogeneous_fbm":
+ # Heterogeneous FBM noise
+ shader_noise = cls.fbm_noise(
+ p, scale, warp_strength, phase_shift, octaves, time, device
+ )
+ elif shader_type == "projection_3d":
+ # 3D projection noise
+ shader_noise = cls.projection3d(
+ p, scale, warp_strength, phase_shift, octaves, time, device
+ )
+ else:
+ # Raise error instead of falling back to random noise
+ raise ValueError(f"Shader type '{shader_type}' is not directly supported in shader_noise_to_tensor. Please use shader_noise_to_tensor_with_params or ensure the shader type is registered.")
+
+ # Convert from [B, H, W, 1] to [B, 1, H, W]
+ shader_noise = shader_noise.permute(0, 3, 1, 2)
+
+ # Normalize to have mean 0 and std 1 (like typical noise)
+ shader_noise = (shader_noise - shader_noise.mean()) / (shader_noise.std() + 1e-8)
+
+ # Expand channels if needed
+ if shader_noise.shape[1] == 1:
+ # Expand to 9 channels for latent space operations instead of just 4
+ shader_noise = shader_noise.expand(-1, 9, -1, -1) # [B, 1, H, W] -> [B, 9, H, W]
+ # If we have 4 channels but need 9 for latent operations
+ elif shader_noise.shape[1] == 4:
+ # Get existing channels
+ r, g, b, a = shader_noise.chunk(4, dim=1)
+
+ # Create 5 more channels as variations of the existing ones
+ c5 = (r + g) / 2.0 # Average of red and green
+ c6 = (g + b) / 2.0 # Average of green and blue
+ c7 = (b + a) / 2.0 # Average of blue and alpha
+ c8 = (r + b) / 2.0 # Average of red and blue
+ c9 = (r + g + b + a) / 4.0 # Average of all channels
+
+ # Combine all 9 channels
+ shader_noise = torch.cat([r, g, b, a, c5, c6, c7, c8, c9], dim=1)
+
+ return shader_noise
+
+ @classmethod
+ def tensor_field(cls, p, viz_type, scale, warp_strength, phase_shift, time, device):
+ """
+ Simple implementation of tensor field visualization
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ viz_type: Visualization type (0-3)
+ scale: Scale factor
+ warp_strength: Warp amount
+ phase_shift: Contrast adjustment
+ time: Animation parameter
+ device: Torch device
+
+ Returns:
+ Field tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = p.shape
+
+ # Scale coordinates by user parameter
+ p = p * scale
+
+ # Apply domain warping if warp strength > 0
+ if warp_strength > 0.0:
+ # Create a smooth flow field for warping
+ warp_coords = p * 0.3 + torch.tensor([time * 0.05, 0.0], device=device).reshape(1, 1, 1, 2)
+ warp_noise = cls.simplex_noise(warp_coords)
+
+ # Apply warp to coordinates
+ p = p + warp_noise * warp_strength
+
+ # Calculate angle field (similar to atan2 in shaders)
+ angle = torch.atan2(p[:, :, :, 1], p[:, :, :, 0])
+
+ # Calculate sines and cosines with 2 offset frequencies
+ sin1 = torch.sin(angle + time * 0.1)
+ cos1 = torch.cos(angle + time * 0.1)
+ sin2 = torch.sin(angle * 2.0 + time * 0.2)
+ cos2 = torch.cos(angle * 2.0 + time * 0.2)
+
+ # Different visualization types
+ if viz_type == 0:
+ # Simple sine wave
+ result = sin1
+ elif viz_type == 1:
+ # Combined sine waves
+ result = sin1 * cos2
+ elif viz_type == 2:
+ # Squared combination
+ result = sin1 * sin1 + cos2 * cos2
+ else: # viz_type == 3 (default - ellipses)
+ # Create elliptical patterns
+ radius = torch.sqrt(torch.sum(p * p, dim=-1, keepdim=True))
+ radius = radius + 0.01 # Avoid division by zero
+ result = (sin1 + cos2) / radius
+
+ # Apply phase shift (contrast adjustment)
+ contrast = 1.0 + phase_shift
+ result = result * contrast
+
+ # Ensure output is in valid [-1, 1] range
+ result = torch.clamp(result, -1.0, 1.0)
+
+ return result
+
+ @classmethod
+ def fbm_noise(cls, p, scale, warp_strength, phase_shift, octaves, time, device):
+ """
+ Heterogeneous FBM noise implementation
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ scale: Scale factor
+ warp_strength: Warp amount
+ phase_shift: Contrast adjustment
+ octaves: Number of detail levels
+ time: Animation time
+ device: Torch device
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = p.shape
+
+ # Scale coordinates by user parameter
+ p = p * scale
+
+ # Apply domain warping if warp strength > 0
+ if warp_strength > 0.0:
+ # Create a smooth flow field for warping
+ warp_coords = p * 0.3 + torch.tensor([time * 0.05, 0.0], device=device).reshape(1, 1, 1, 2)
+ warp_noise = cls.simplex_noise(warp_coords)
+
+ # Apply warp to coordinates
+ p = p + warp_noise * warp_strength
+
+ # Generate multi-octave FBM noise
+ result = torch.zeros(batch, height, width, 1, device=device)
+ amplitude = 1.0
+ frequency = 1.0
+
+ # Loop through octaves
+ for i in range(min(int(octaves), 8)):
+ current_p = p * frequency + torch.tensor([i * 2.0, time * 0.1], device=device).reshape(1, 1, 1, 2)
+ value = cls.simplex_noise(current_p)
+
+ # Calculate heterogeneous weight
+ if i > 0:
+ # This creates areas with more or less detail
+ weight = torch.clamp(cls.simplex_noise(p * 0.1 + i * 0.5), 0.0, 1.0)
+ value = value * weight
+
+ # Add weighted noise at current frequency
+ result += amplitude * value
+
+ # Prepare for next octave
+ frequency *= 2.0
+ amplitude *= 0.5
+
+ # Apply phase shift (contrast adjustment)
+ contrast = 1.0 + phase_shift
+ result = result * contrast
+
+ # Ensure output is in valid [-1, 1] range
+ result = torch.clamp(result, -1.0, 1.0)
+
+ return result
+
+ @classmethod
+ def projection3d(cls, p, scale, warp_strength, phase_shift, octaves, time, device):
+ """
+ 3D projection noise implementation
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ scale: Scale factor
+ warp_strength: Warp amount
+ phase_shift: Contrast adjustment
+ octaves: Number of detail levels
+ time: Animation time
+ device: Torch device
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = p.shape
+
+ # Scale coordinates by user parameter
+ p = p * scale
+
+ # Apply domain warping if warp strength > 0
+ if warp_strength > 0.0:
+ # Create a smooth flow field for warping
+ warp_coords1 = p * 0.3 + torch.tensor([0.0, time * 0.05], device=device).reshape(1, 1, 1, 2)
+ warp_coords2 = p * 0.3 + torch.tensor([time * 0.05, 0.0], device=device).reshape(1, 1, 1, 2)
+
+ # Generate noise for warping
+ warp_noise1 = cls.simplex_noise(warp_coords1)
+ warp_noise2 = cls.simplex_noise(warp_coords2, seed=13.5)
+
+ # Apply warp to coordinates
+ p = p + torch.cat([warp_noise1, warp_noise2], dim=-1) * warp_strength
+
+ # Create 3D coordinates for projection
+ z = torch.ones(batch, height, width, 1, device=device) * (time * 0.1)
+ coords_3d = torch.cat([p, z], dim=-1)
+
+ # Generate multi-octave 3D noise
+ result = torch.zeros(batch, height, width, 1, device=device)
+ amplitude = 1.0
+ frequency = 1.0
+
+ # Loop through octaves
+ for i in range(min(int(octaves), 8)):
+ # Scale and add phase offset for each octave
+ current_coords = coords_3d * frequency
+ value = cls.simplex_noise_3d(current_coords, seed=i * 13.5)
+
+ # Add to result with decreasing amplitude
+ result += amplitude * value
+
+ # Prepare for next octave
+ frequency *= 2.0
+ amplitude *= 0.5
+
+ # Apply phase shift (contrast adjustment)
+ contrast = 1.0 + phase_shift
+ result = result * contrast
+
+ # Ensure output is in valid [-1, 1] range
+ result = torch.clamp(result, -1.0, 1.0)
+
+ return result
+
+ @classmethod
+ def simplex_noise(cls, coords, seed=0):
+ """
+ Simplex-like noise implementation in PyTorch
+
+ Args:
+ coords: Coordinate tensor [batch, height, width, 2]
+ seed: Random seed value
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = coords.shape
+ device = coords.device
+
+ # Integer and fractional parts
+ i0 = torch.floor(coords)
+ i1 = i0 + 1.0
+ f0 = coords - i0
+
+ # Dot products with random gradients
+ def random_gradient(p, seed_val=seed):
+ # Simple hash function
+ h = p[:, :, :, 0] * 15.0 + p[:, :, :, 1] * 37.0 + seed_val
+ h = torch.sin(h) * 43758.5453
+ angle = h * 2.0 * math.pi
+ return torch.stack([torch.cos(angle), torch.sin(angle)], dim=-1)
+
+ # Get gradients at corners
+ g00 = random_gradient(i0)
+ g10 = random_gradient(torch.cat([i1[:, :, :, 0:1], i0[:, :, :, 1:2]], dim=-1))
+ g01 = random_gradient(torch.cat([i0[:, :, :, 0:1], i1[:, :, :, 1:2]], dim=-1))
+ g11 = random_gradient(i1)
+
+ # Calculate contributions from each corner
+ def contribution(grad, pos, point):
+ # Calculate falloff
+ t = 0.5 - torch.sum((point - pos) ** 2, dim=-1, keepdim=True)
+ t = torch.maximum(t, torch.zeros_like(t))
+ t = t * t * t * t # Quintic interpolation
+
+ # Dot product of gradient and offset
+ offset = point - pos
+ return t * torch.sum(grad * offset, dim=-1, keepdim=True)
+
+ # Get contributions
+ n00 = contribution(g00, i0, coords)
+ n10 = contribution(g10, torch.cat([i1[:, :, :, 0:1], i0[:, :, :, 1:2]], dim=-1), coords)
+ n01 = contribution(g01, torch.cat([i0[:, :, :, 0:1], i1[:, :, :, 1:2]], dim=-1), coords)
+ n11 = contribution(g11, i1, coords)
+
+ # Smoothstep for interpolation
+ t = f0 * f0 * f0 * (f0 * (f0 * 6.0 - 15.0) + 10.0)
+
+ # Bilinear interpolation with smoothstep
+ n_x0 = n00 * (1.0 - t[:, :, :, 0:1]) + n10 * t[:, :, :, 0:1]
+ n_x1 = n01 * (1.0 - t[:, :, :, 0:1]) + n11 * t[:, :, :, 0:1]
+ noise = n_x0 * (1.0 - t[:, :, :, 1:2]) + n_x1 * t[:, :, :, 1:2]
+
+ # Scale to match WebGL shader output range
+ return 2.0 * noise
+
+ @classmethod
+ def simplex_noise_3d(cls, coords, seed=0):
+ """
+ Simplified 3D noise implementation
+ This is a basic approximation for the 3D projection
+
+ Args:
+ coords: 3D coordinate tensor [batch, height, width, 3]
+ seed: Random seed value
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = coords.shape
+ device = coords.device
+
+ # Slice the 3D space in multiple 2D planes
+ xy_coords = coords[:, :, :, 0:2]
+ yz_coords = coords[:, :, :, 1:3]
+ xz_coords = torch.cat([coords[:, :, :, 0:1], coords[:, :, :, 2:3]], dim=-1)
+
+ # Get 2D noise for each plane
+ xy_noise = cls.simplex_noise(xy_coords, seed=seed)
+ yz_noise = cls.simplex_noise(yz_coords, seed=seed+1)
+ xz_noise = cls.simplex_noise(xz_coords, seed=seed+2)
+
+ # Combine the noises
+ noise = (xy_noise + yz_noise + xz_noise) / 3.0
+
+ return noise
+
+ @classmethod
+ def shader_noise_to_tensor_with_params(cls, batch_size=1, height=64, width=64, shader_type="tensor_field",
+ shader_params=None, time=0.0, device="cuda", seed=0):
+ """
+ Generate noise tensors using the full shader_params dictionary
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_type: Type of shader ("tensor_field", "heterogeneous_fbm", "projection_3d", "cellular", "fractal", "perlin", "waves", "gaussian")
+ shader_params: Full dictionary of shader parameters from JSON
+ time: Animation time
+ device: Device to create tensor on
+ seed: Random seed
+
+ Returns:
+ Tensor with shape [batch_size, height, width, 1] in BHWC format
+ """
+ # Fallback to empty dict if None
+ if shader_params is None:
+ shader_params = {}
+
+ # Extract common parameters with fallbacks
+ scale = shader_params.get("shaderScale", shader_params.get("scale", 1.0))
+ phase_shift = shader_params.get("shaderPhaseShift", shader_params.get("phase_shift", 0.0))
+ warp_strength = shader_params.get("shaderWarpStrength", shader_params.get("warp_strength", 0.0))
+ octaves = shader_params.get("shaderOctaves", shader_params.get("octaves", 3.0))
+ visualization_type = shader_params.get("visualization_type", 3)
+
+ # Extract shape parameters for all noise types
+ shape_type = shader_params.get("shaderShapeType", shader_params.get("shape_type", "none"))
+ shape_strength = shader_params.get("shaderShapeStrength", shader_params.get("shapemaskstrength", shader_params.get("shape_strength", 1.0)))
+
+ # Ensure consistent parameter naming in shader_params
+ shader_params["shaderShapeType"] = shape_type
+ shader_params["shaderShapeStrength"] = shape_strength
+
+ # Extract temporal coherence parameters
+ use_temporal_coherence = shader_params.get("useTemporalCoherence", shader_params.get("temporal_coherence", False))
+ base_seed = shader_params.get("base_seed", seed)
+
+ # Make sure we have a proper time parameter for animation/temporal coherence
+ if "time" not in shader_params or shader_params["time"] is None:
+ shader_params["time"] = time
+ else:
+ # If time is already in shader_params, ensure it's used
+ time = shader_params["time"]
+
+ # Extract color parameters for debugging
+ color_scheme = shader_params.get("colorScheme", "none")
+ color_intensity = shader_params.get("shaderColorIntensity", 0.8)
+
+ # Set deterministic seed for this operation
+ # Use base_seed if temporal coherence is enabled
+ effective_seed = base_seed if use_temporal_coherence else seed
+ torch.manual_seed(effective_seed)
+
+ # Use the specialized methods for specific shader types
+ if shader_type == "cellular" and hasattr(cls, 'cellular_noise_with_params'):
+ noise_bhwc = cls.cellular_noise_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif shader_type == "domain_warp" and hasattr(cls, 'domain_warp_with_params'):
+ noise_bhwc = cls.domain_warp_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif shader_type == "fractal" and hasattr(cls, 'fractal_noise_with_params'):
+ noise_bhwc = cls.fractal_noise_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif shader_type == "perlin" and hasattr(cls, 'perlin_noise_with_params'):
+ noise_bhwc = cls.perlin_noise_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif shader_type == "waves" and hasattr(cls, 'waves_noise_with_params'):
+ noise_bhwc = cls.waves_noise_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif shader_type == "gaussian" and hasattr(cls, 'gaussian_noise_with_params'):
+ noise_bhwc = cls.gaussian_noise_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif shader_type == "tensor_field" and hasattr(cls, 'tensor_field_with_params'):
+ noise_bhwc = cls.tensor_field_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif shader_type == "heterogeneous_fbm" and hasattr(cls, 'heterogeneous_fbm_with_params'):
+ noise_bhwc = cls.heterogeneous_fbm_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif shader_type == "interference_patterns" and hasattr(cls, 'interference_patterns_with_params'):
+ noise_bhwc = cls.interference_patterns_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif (shader_type == "spectral" or shader_type == "spectral_noise") and hasattr(cls, 'spectral_noise_with_params'):
+ noise_bhwc = cls.spectral_noise_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif (shader_type == "projection_3d" or shader_type == "3d_projection") and hasattr(cls, 'projection_3d_with_params'):
+ noise_bhwc = cls.projection_3d_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ elif (shader_type == "curl" or shader_type == "curl_noise") and hasattr(cls, 'curl_noise_with_params'):
+ noise_bhwc = cls.curl_noise_with_params(batch_size, height, width, shader_params, time, device, effective_seed)
+ else:
+ # Use the standard method for other shader types
+ noise_bhwc = cls.shader_noise_to_tensor(
+ batch_size=batch_size,
+ height=height,
+ width=width,
+ shader_type=shader_type,
+ visualization_type=visualization_type,
+ scale=scale,
+ phase_shift=phase_shift,
+ warp_strength=warp_strength,
+ time=time,
+ device=device,
+ seed=effective_seed,
+ octaves=octaves
+ )
+
+ # Convert from BHWC to BCHW for color processing
+ noise_bchw = noise_bhwc.permute(0, 3, 1, 2)
+
+ # Expand channels if needed for latent space operations
+ if noise_bchw.shape[1] == 1:
+ # Expand to 9 channels for latent space operations instead of just 4
+ noise_bchw = noise_bchw.expand(-1, 9, -1, -1)
+ # If we have 4 channels but need 9 for latent operations
+ elif noise_bchw.shape[1] == 4:
+ # Get existing channels
+ r, g, b, a = noise_bchw.chunk(4, dim=1)
+
+ # Create 5 more channels as variations of the existing ones
+ c5 = (r + g) / 2.0 # Average of red and green
+ c6 = (g + b) / 2.0 # Average of green and blue
+ c7 = (b + a) / 2.0 # Average of blue and alpha
+ c8 = (r + b) / 2.0 # Average of red and blue
+ c9 = (r + g + b + a) / 4.0 # Average of all channels
+
+ # Combine all 9 channels
+ noise_bchw = torch.cat([r, g, b, a, c5, c6, c7, c8, c9], dim=1)
+
+ # Apply color scheme if we have one
+ if color_scheme != "none" and color_intensity > 0:
+ # Import here to avoid circular import
+ from .shader_params_reader import ShaderParamsReader
+
+ # Get stats before color application
+ before_min = noise_bchw.min().item()
+ before_max = noise_bchw.max().item()
+ before_mean = noise_bchw.mean().item()
+ before_std = noise_bchw.std().item()
+
+ # Apply color transformation
+ noise_bchw = ShaderParamsReader.apply_color_scheme(noise_bchw, shader_params)
+
+ # Get stats after color application
+ after_min = noise_bchw.min().item()
+ after_max = noise_bchw.max().item()
+ after_mean = noise_bchw.mean().item()
+ after_std = noise_bchw.std().item()
+
+ # Check if the stats changed significantly (which would indicate the color was applied)
+ if abs(before_std - after_std) < 0.001 and abs(before_mean - after_mean) < 0.001:
+ pass
+ else:
+ pass
+
+ # Convert back to BHWC format for return
+ noise_bhwc = noise_bchw.permute(0, 2, 3, 1)
+
+ return noise_bhwc
+
+ @classmethod
+ def curl_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ """
+ Method that directly takes the full shader_params dictionary
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Full parameter dictionary from JSON
+ time: Animation time
+ device: Device to create tensor on
+ seed: Random seed
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ # Add time to shader params if not already present
+ if "time" not in shader_params:
+ shader_params["time"] = time
+
+ # Ensure shape parameters are properly extracted and included
+ shape_type = shader_params.get("shaderShapeType", shader_params.get("shape_type", "none"))
+ shape_mask_strength = shader_params.get("shaderShapeStrength", shader_params.get("shapemaskstrength", 1.0))
+
+ # Ensure phase shift is properly extracted and used with the correct parameter name
+ phase_shift = shader_params.get("shaderPhaseShift", shader_params.get("phase_shift", 0.5))
+
+ # Extract temporal coherence parameters
+ base_seed = shader_params.get("base_seed", seed) # Use provided seed as default base_seed
+ use_temporal_coherence = shader_params.get("temporal_coherence", shader_params.get("useTemporalCoherence", False))
+
+ # Set deterministic seed for this operation
+ effective_seed = base_seed if use_temporal_coherence else seed
+
+ # Make sure the correct parameter names are set for consistency
+ shader_params["shaderShapeType"] = shape_type
+ shader_params["shaderShapeStrength"] = shape_mask_strength
+ shader_params["shaderPhaseShift"] = phase_shift # Ensure it's consistently named
+ shader_params["base_seed"] = base_seed # Ensure base_seed is set
+ shader_params["temporal_coherence"] = use_temporal_coherence # Ensure temporal_coherence is set
+
+ # Determine target channels for the generator call
+ # Prioritize from shader_params, then ACEStep/ACE logic, then default.
+ gen_target_channels = 4 # Default
+ if "target_channels" in shader_params:
+ gen_target_channels = int(shader_params["target_channels"])
+ elif shader_params.get("inner_model_class") in ["ACEStep", "ACE"]:
+ gen_target_channels = 8
+
+ # Use the CurlNoiseGenerator directly with the full params
+ from .shaders.curl_noise import CurlNoiseGenerator
+ curl_noise = CurlNoiseGenerator.get_curl_noise(
+ batch_size=batch_size,
+ height=height,
+ width=width,
+ shader_params=shader_params, # Pass the full parameter dictionary
+ device=device,
+ seed=effective_seed, # Use base_seed if temporal coherence is enabled
+ target_channels=gen_target_channels # Explicitly pass determined target channels
+ )
+
+ # Convert to BHWC format to match expected output
+ result = curl_noise.permute(0, 2, 3, 1)[:, :, :, 0:1]
+
+ return result
\ No newline at end of file
diff --git a/shaders/curl_noise.py b/shaders/curl_noise.py
new file mode 100644
index 0000000..c8d7a23
--- /dev/null
+++ b/shaders/curl_noise.py
@@ -0,0 +1,2447 @@
+import torch
+import torch.nn.functional as F
+import math
+
+class CurlNoiseGenerator:
+ """
+ PyTorch implementation of Curl Noise that closely matches
+ the WebGL Curl Noise shader from shader_renderer.js
+
+ This class generates fluid-like curl noise by computing
+ divergence-free vector fields and advecting properties along them.
+ Used to influence the sampling process in image generation.
+
+ Now enhanced with true temporal coherence for smooth animations.
+ """
+
+ @staticmethod
+ def get_curl_noise(batch_size, height, width, shader_params, device="cuda", seed=0, target_channels=4):
+ """
+ Generate curl noise tensor that matches the WebGL implementation
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Dictionary containing parameters from shader_params.json.
+ Should include 'target_channels' if determined upstream.
+ device: Device to create tensor on
+ seed: Random seed for deterministic results
+ target_channels: Fallback number of channels if not in shader_params (default: 4)
+
+ Returns:
+ Tensor with shape [batch_size, target_channels, height, width]
+ """
+ # Get debugger instance
+ # debugger = get_debugger()
+
+ # --- Helper function for smoothstep ---
+ def smoothstep(edge0, edge1, x):
+ # Ensure edges are tensors for broadcasting with x
+ if not isinstance(edge0, torch.Tensor):
+ # Use x's properties for the new tensor
+ edge0 = torch.full_like(x, float(edge0), device=x.device, dtype=x.dtype)
+ if not isinstance(edge1, torch.Tensor):
+ edge1 = torch.full_like(x, float(edge1), device=x.device, dtype=x.dtype)
+
+ # Calculate t, handling potential edge0 >= edge1 cases by clamping
+ delta = edge1 - edge0
+ # Avoid division by zero or near-zero, maintain sign for correct 1-smoothstep
+ safe_delta = torch.where(torch.abs(delta) < 1e-8, torch.sign(delta) * 1e-8 + 1e-8*(1-torch.abs(torch.sign(delta))), delta)
+ t = torch.clamp((x - edge0) / safe_delta, 0.0, 1.0)
+
+ return t * t * (3.0 - 2.0 * t)
+ # -------------------------------------
+
+ # --- Parameter Extraction --- Corrected Keys ---
+ scale = shader_params.get("scale", 1.0)
+ # Check for both 'warp_strength' and 'warp' keys
+ warp_strength = shader_params.get("warp_strength", shader_params.get("warp", 0.5))
+ # Check for both 'phase_shift' and 'phase' keys
+ phase_shift = shader_params.get("phase_shift", shader_params.get("phase", 0.5))
+ octaves = int(shader_params.get("octaves", 3))
+ time = shader_params.get("time", 0.0)
+
+ # Extract temporal coherence parameters (base_seed is used across frames)
+ base_seed = shader_params.get("base_seed", seed)
+ use_temporal_coherence = shader_params.get("useTemporalCoherence", False)
+
+ # Extract shape mask parameters
+ shape_type = shader_params.get("shape_type", "none")
+ # Check for both 'shapemaskstrength' and 'shape_strength' keys
+ shape_mask_strength = shader_params.get("shapemaskstrength", shader_params.get("shape_strength", 1.0))
+
+ # Extract color scheme parameters - Check both JS and Python style keys
+ color_scheme = shader_params.get("colorScheme", shader_params.get("color_scheme", "none"))
+ color_intensity = shader_params.get("shaderColorIntensity", shader_params.get("color_intensity", 0.8))
+
+ # --- Channel Detection Logic ---
+ # Prioritize target_channels from shader_params if available (set upstream)
+ if "target_channels" in shader_params:
+ detected_channels = int(shader_params["target_channels"])
+ if detected_channels != target_channels: # Log if different from function arg
+ print(f"CurlNoise: Channel mismatch - function arg {target_channels}, shader_params {detected_channels}. Using shader_params.") # Added print
+ target_channels = detected_channels
+ else:
+ # Fallback to function argument if not in shader_params
+ print(f"CurlNoise: Using target_channels {target_channels} from function argument (not in shader_params).") # Added print
+ # Optional: Add back minimal name-based detection here as a last resort?
+ # For now, just rely on the default passed to the function.
+
+ # --- Model Specific Channel Overrides ---
+ # Check if the model is CosmosVideo and override the channel count if needed
+ model_class = shader_params.get("model_class", "")
+ inner_model_class = shader_params.get("inner_model_class", "")
+
+ if inner_model_class == "CosmosVideo":
+ target_channels = 16
+ # Update shader_params as well for consistency downstream
+ shader_params["target_channels"] = 16
+ elif model_class == "CosmosVideo": # Fallback check on outer model class
+ target_channels = 16
+ shader_params["target_channels"] = 16
+ elif inner_model_class == "ACEStep":
+ target_channels = 8
+ shader_params["target_channels"] = 8
+
+ # Print params being used for debugging
+ # print(f"CurlNoise: Using shader_type: {shader_params.get('shader_type', 'curl_noise')}") # Added print
+ # print(f"CurlNoise: Scale={scale}, Warp Strength={warp_strength}, Phase Shift={phase_shift}, Octaves={octaves}") # Modified print
+ # print(f"CurlNoise: Temporal settings: time={time}, base_seed={base_seed}, coherence={use_temporal_coherence}") # Modified print
+ # print(f"CurlNoise: Shape Mask: type={shape_type}, strength={shape_mask_strength}") # Modified print
+ # print(f"CurlNoise: Color Scheme: scheme={color_scheme}, intensity={color_intensity}") # Modified print
+ # print(f"CurlNoise: Target Channels: {target_channels}") # Modified print
+
+ # Create coordinate grid (normalized to [0, 1])
+ y_coords = torch.linspace(0, 1, height, device=device).view(1, 1, height, 1).expand(batch_size, 1, height, width)
+ x_coords = torch.linspace(0, 1, width, device=device).view(1, 1, 1, width).expand(batch_size, 1, height, width)
+ coords = torch.cat((x_coords, y_coords), dim=1)
+
+ # Track the initial coordinate grid tensor
+ # debugger.track_tensor_shape_history(coords, "coords_grid", "initial_creation")
+
+ # Apply curl noise with FBM to get velocity field
+ coords_bhwc = coords.permute(0, 2, 3, 1) # Convert to [batch, height, width, 2]
+
+ # Set random seed for this operation to ensure deterministic results
+ torch.manual_seed(base_seed)
+
+ # Store coords_bhwc as p for later use in additional channel generation
+ p = coords_bhwc
+
+ # --- Refactored Velocity and Advection ---
+ # 1. Get the base velocity field using scaled coordinates
+ base_velocity = CurlNoiseGenerator.get_velocity_field(
+ p * scale, # Scale coordinates here for velocity generation
+ time,
+ octaves,
+ device,
+ base_seed, # Use base_seed for consistent velocity field across frames if temporal
+ use_temporal_coherence
+ )
+ # debugger.track_tensor_shape_history(base_velocity, "base_velocity", "after_get_velocity_field")
+
+ # 2. Apply warp intensity using warp_strength
+ warped_velocity = CurlNoiseGenerator.apply_warp_intensity(base_velocity, warp_strength)
+ # debugger.track_tensor_shape_history(warped_velocity, "warped_velocity", "after_apply_warp_intensity")
+
+ # The 'velocity' variable will now refer to the *warped* velocity for subsequent steps
+ velocity = warped_velocity # BHWC format [B, H, W, 2]
+
+ # --- Apply shape mask (to the warped velocity) ---
+ shape_mask = None
+ shape_factor = 1.0 # shape_factor is not used correctly later, lerp is better
+ if shape_type not in ["none", "0"] and shape_mask_strength > 0:
+ # Create the mask based on coordinates
+ shape_mask = None
+
+ # Helper function for random values based on coords and seed
+ def random_val(coords, seed_offset):
+ torch.manual_seed(base_seed + seed_offset)
+ # Use a simple hash-like function based on coordinates
+ hash_val = torch.sin(coords[:, :, :, 0] * (12.9898 + seed_offset) + coords[:, :, :, 1] * (78.233 + seed_offset)) * 43758.5453
+ return torch.frac(hash_val)
+
+ if shape_type == "circle": # Existing 'radial' in JS is actually circle
+ center_x, center_y = 0.5, 0.5
+ y_diff = coords_bhwc[:, :, :, 1] - center_y
+ x_diff = coords_bhwc[:, :, :, 0] - center_x
+ dist = torch.sqrt(x_diff**2 + y_diff**2)
+ shape_mask = 1.0 - torch.clamp(dist * 2, 0, 1) # Match GLSL clamp
+ elif shape_type == "square": # Not in JS, but kept for potential use
+ x_mask = torch.abs(coords_bhwc[:, :, :, 0] - 0.5) * 2
+ y_mask = torch.abs(coords_bhwc[:, :, :, 1] - 0.5) * 2
+ dist = torch.max(x_mask, y_mask)
+ shape_mask = 1.0 - torch.clamp(dist, 0, 1)
+ elif shape_type == "radial": # Actual radial gradient
+ # Convert time to tensor
+ time_tensor = torch.tensor(time, device=device)
+ center_x, center_y = 0.5, 0.5
+ center_x += 0.2 * torch.cos(time_tensor)
+ center_y += 0.2 * torch.sin(time_tensor)
+ y_diff = coords_bhwc[:, :, :, 1] - center_y
+ x_diff = coords_bhwc[:, :, :, 0] - center_x
+ dist = torch.sqrt(x_diff**2 + y_diff**2) * 2.0
+ shape_mask = torch.clamp(1.0 - dist, 0.0, 1.0)
+ elif shape_type == "linear":
+ # Convert time calculation to tensor for torch.fmod
+ time_tensor_02 = torch.tensor(time * 0.2, device=device)
+ x_offset = torch.fmod(time_tensor_02, 1.0) * 2.0 # Match JS fract animation
+ shifted_x = torch.fmod(coords_bhwc[:, :, :, 0] + x_offset, 1.0)
+ shape_mask = shifted_x
+ elif shape_type == "spiral":
+ centered = coords_bhwc - 0.5
+ theta = torch.atan2(centered[:, :, :, 1], centered[:, :, :, 0])
+ r = torch.norm(centered, dim=-1) * 2.0
+ # Convert time to tensor
+ time_tensor = torch.tensor(time, device=device)
+ theta += time_tensor
+ shape_mask = torch.fmod((theta / (2.0 * math.pi) + r), 1.0)
+ elif shape_type == "checkerboard":
+ grid_size = 8.0
+ # Convert time expressions to tensors
+ time_tensor_gs_02 = torch.tensor(time * grid_size * 0.2, device=device)
+ time_tensor_gs_01 = torch.tensor(time * grid_size * 0.1, device=device)
+ x_offset = time_tensor_gs_02
+ y_offset = time_tensor_gs_01
+ x_grid = torch.floor((coords_bhwc[:, :, :, 0] + x_offset / grid_size) * grid_size) * 0.5
+ y_grid = torch.floor((coords_bhwc[:, :, :, 1] + y_offset / grid_size) * grid_size) * 0.5
+ shape_mask = torch.fmod(x_grid + y_grid, 1.0)
+ elif shape_type == "spots":
+ mask = torch.zeros_like(coords_bhwc[:, :, :, 0])
+ num_spots = 10
+ # Convert time to tensor once outside the loop
+ time_tensor = torch.tensor(time, device=device)
+ for i in range(num_spots):
+ rand_x = random_val(coords_bhwc, i * 78)
+ rand_y = random_val(coords_bhwc, i * 12)
+ size = (random_val(coords_bhwc, i * 93) * 0.3 + 0.1) # Base size
+
+ angle_float = time + float(i)
+ angle_tensor = torch.tensor(angle_float, device=device) # Convert angle to tensor
+ spot_pos_x = 0.5 + torch.cos(angle_tensor) * 0.4 * rand_x
+ spot_pos_y = 0.5 + torch.sin(angle_tensor) * 0.4 * rand_y
+
+ # Convert time expression to tensor for size calculation
+ size_angle_tensor = torch.tensor(time * 2.0 + float(i), device=device)
+ size *= 1.0 + 0.2 * torch.sin(size_angle_tensor) # Pulsing size
+
+ dist = torch.sqrt((coords_bhwc[:, :, :, 0] - spot_pos_x)**2 + (coords_bhwc[:, :, :, 1] - spot_pos_y)**2)
+ star_mask = torch.clamp(1.0 - dist / size, 0.0, 1.0)
+ mask = torch.maximum(mask, star_mask)
+ shape_mask = mask
+ elif shape_type == "hexgrid":
+ hex_uv = coords_bhwc * 6.0
+ # Convert time expressions to tensors before sin/cos
+ time_tensor_05 = torch.tensor(time * 0.5, device=device)
+ time_tensor_03 = torch.tensor(time * 0.3, device=device)
+ time_tensor = torch.tensor(time, device=device)
+
+ hex_uv[:, :, :, 0] += torch.sin(time_tensor_05) * 0.5
+ hex_uv[:, :, :, 1] += torch.cos(time_tensor_03) * 0.5
+
+ r = torch.tensor([1.0, 1.73], device=device).reshape(1, 1, 1, 2)
+ h = r * 0.5
+ a = torch.fmod(hex_uv, r) - h
+ b = torch.fmod(hex_uv + h, r) - h
+
+ dist = torch.minimum(torch.norm(a, dim=-1), torch.norm(b, dim=-1))
+ cell_size = 0.3 + 0.1 * torch.sin(time_tensor)
+ shape_mask = smoothstep(cell_size + 0.05, cell_size - 0.05, dist)
+ elif shape_type == "stripes":
+ freq = 10.0
+ # Convert time expressions to tensors
+ time_tensor = torch.tensor(time, device=device)
+ time_tensor_02 = torch.tensor(time * 0.2, device=device)
+ angle = 0.5 * torch.sin(time_tensor_02)
+ cos_a = torch.cos(angle)
+ sin_a = torch.sin(angle)
+ rotated_x = coords_bhwc[:, :, :, 0] * cos_a - coords_bhwc[:, :, :, 1] * sin_a
+ stripes = torch.sin(rotated_x * freq + time_tensor) # Use time_tensor here
+ shape_mask = smoothstep(0.0, 0.1, stripes) * smoothstep(0.0, -0.1, -stripes)
+ elif shape_type == "gradient": # Renamed from JS gradient_x/y to just gradient
+ # Convert time expression to tensor
+ time_tensor_02 = torch.tensor(time * 0.2, device=device)
+ angle = time_tensor_02
+ dir_x = torch.cos(angle)
+ dir_y = torch.sin(angle)
+ proj = (coords_bhwc[:, :, :, 0] - 0.5) * dir_x + (coords_bhwc[:, :, :, 1] - 0.5) * dir_y + 0.5
+ shape_mask = proj
+ elif shape_type == "vignette":
+ # Convert time expressions to tensors
+ time_tensor_03 = torch.tensor(time * 0.3, device=device)
+ time_tensor_04 = torch.tensor(time * 0.4, device=device)
+ time_tensor_05 = torch.tensor(time * 0.5, device=device)
+ center_x = 0.5 + 0.2 * torch.sin(time_tensor_03)
+ center_y = 0.5 + 0.2 * torch.cos(time_tensor_04)
+ dist = torch.sqrt((coords_bhwc[:, :, :, 0] - center_x)**2 + (coords_bhwc[:, :, :, 1] - center_y)**2)
+ radius = 0.6 + 0.2 * torch.sin(time_tensor_05)
+ smoothness = 0.3
+ shape_mask = 1.0 - smoothstep(radius - smoothness, radius, dist)
+ elif shape_type == "cross":
+ # Convert time expressions to tensors
+ time_tensor = torch.tensor(time, device=device)
+ time_tensor_02 = torch.tensor(time * 0.2, device=device)
+ thickness = 0.1 + 0.05 * torch.sin(time_tensor)
+ rotation = time_tensor_02
+ cos_r = torch.cos(rotation)
+ sin_r = torch.sin(rotation)
+ centered_x = coords_bhwc[:, :, :, 0] - 0.5
+ centered_y = coords_bhwc[:, :, :, 1] - 0.5
+ rotated_x = centered_x * cos_r - centered_y * sin_r + 0.5
+ rotated_y = centered_x * sin_r + centered_y * cos_r + 0.5
+
+ h_bar = smoothstep(0.5 - thickness, 0.5 - thickness + 0.02, rotated_y) * \
+ smoothstep(0.5 + thickness, 0.5 + thickness - 0.02, rotated_y)
+ v_bar = smoothstep(0.5 - thickness, 0.5 - thickness + 0.02, rotated_x) * \
+ smoothstep(0.5 + thickness, 0.5 + thickness - 0.02, rotated_x)
+ shape_mask = torch.maximum(h_bar, v_bar)
+ elif shape_type == "stars":
+ mask = torch.zeros_like(coords_bhwc[:, :, :, 0])
+ num_stars = 20
+ # Convert time to tensor once outside the loop
+ time_tensor = torch.tensor(time, device=device)
+ time_tensor_01 = torch.tensor(time * 0.1, device=device)
+ time_tensor_015 = torch.tensor(time * 0.15, device=device)
+ for i in range(num_stars):
+ rand_x = random_val(coords_bhwc, i * 78 + 10)
+ rand_y = random_val(coords_bhwc, i * 12 + 20)
+ # Convert time expressions to tensors
+ time_sin_arg = torch.tensor(float(i), device=device) + time_tensor_01
+ time_cos_arg = torch.tensor(float(i) * 1.5, device=device) + time_tensor_015
+ star_pos_x = torch.fmod(rand_x + 0.05 * torch.sin(time_sin_arg), 1.0)
+ star_pos_y = torch.fmod(rand_y + 0.05 * torch.cos(time_cos_arg), 1.0)
+
+ # Convert time expression for brightness to tensor
+ brightness_arg = torch.tensor(float(i), device=device) + time_tensor * (0.5 + rand_x * 0.5)
+ brightness = 0.5 + 0.5 * torch.sin(brightness_arg)
+ size = 0.01 + 0.015 * rand_y * brightness
+
+ dist = torch.sqrt((coords_bhwc[:, :, :, 0] - star_pos_x)**2 + (coords_bhwc[:, :, :, 1] - star_pos_y)**2)
+ star_mask = smoothstep(size, size * 0.5, dist) * brightness
+ mask = torch.maximum(mask, star_mask)
+ shape_mask = mask
+ elif shape_type == "triangles":
+ # Convert time expressions to tensors
+ t_tensor = torch.tensor(time * 0.2, device=device)
+ t_sin_arg = t_tensor
+ t_cos_arg = torch.tensor(time * 0.7, device=device) # Keep original time * 0.7 for cos
+ t_border_arg = torch.tensor(time * 1.5, device=device) # For border width calculation
+ scale_factor = 5.0
+ uv = coords_bhwc * scale_factor
+ uv[:, :, :, 0] += torch.sin(t_sin_arg) * 0.5
+ uv[:, :, :, 1] += torch.cos(t_cos_arg) * 0.5
+
+ gv = torch.fmod(uv, 1.0) - 0.5
+
+ # Simplified distance to triangle edges - not exact but visually similar
+ d1 = torch.abs(gv[:, :, :, 0] + gv[:, :, :, 1])
+ d2 = torch.abs(gv[:, :, :, 0] - gv[:, :, :, 1])
+ d3 = torch.abs(gv[:, :, :, 0]) * 0.866 + torch.abs(gv[:, :, :, 1]) * 0.5
+
+ d = torch.minimum(torch.minimum(d1, d2), d3) * 0.7 # Approximate distance
+
+ border_width = 0.05 + 0.03 * torch.sin(t_border_arg)
+ shape_mask = smoothstep(border_width, border_width - 0.02, d)
+ elif shape_type == "concentric":
+ # Convert time expressions to tensors
+ time_tensor_03 = torch.tensor(time * 0.3, device=device)
+ time_tensor_04 = torch.tensor(time * 0.4, device=device)
+ time_tensor_01 = torch.tensor(time * 0.1, device=device)
+ time_tensor_05 = torch.tensor(time * 0.5, device=device)
+
+ center_x = 0.5 + 0.2 * torch.sin(time_tensor_03)
+ center_y = 0.5 + 0.2 * torch.cos(time_tensor_04)
+ dist = torch.sqrt((coords_bhwc[:, :, :, 0] - center_x)**2 + (coords_bhwc[:, :, :, 1] - center_y)**2)
+ freq = 10.0 + 5.0 * torch.sin(time_tensor_01)
+ phase = time_tensor_05
+ rings = torch.sin(dist * freq + phase)
+ shape_mask = smoothstep(0.0, 0.1, rings) * smoothstep(0.0, -0.1, -rings)
+ elif shape_type == "rays":
+ # Convert time expressions to tensors
+ time_tensor_03 = torch.tensor(time * 0.3, device=device)
+ time_tensor_04 = torch.tensor(time * 0.4, device=device)
+ time_tensor_05 = torch.tensor(time * 0.5, device=device)
+
+ center_x = 0.5 + 0.1 * torch.sin(time_tensor_03)
+ center_y = 0.5 + 0.1 * torch.cos(time_tensor_04)
+ to_center_x = coords_bhwc[:, :, :, 0] - center_x
+ to_center_y = coords_bhwc[:, :, :, 1] - center_y
+ angle = torch.atan2(to_center_y, to_center_x)
+ freq = 8.0
+ phase = time_tensor_05
+ rays_val = torch.sin(angle * freq + phase)
+ dist = torch.sqrt(to_center_x**2 + to_center_y**2)
+ falloff = 1.0 - smoothstep(0.0, 0.8, dist)
+ shape_mask = smoothstep(0.0, 0.3, rays_val) * falloff
+ elif shape_type == "zigzag":
+ freq = 10.0
+ # Convert time expressions to tensors
+ time_tensor = torch.tensor(time, device=device)
+ time_tensor_02 = torch.tensor(time * 0.2, device=device)
+ time_tensor_05 = torch.tensor(time * 0.5, device=device)
+ time_tensor_03 = torch.tensor(time * 0.3, device=device)
+
+ angle = 0.5 * torch.sin(time_tensor_02)
+ cos_a = torch.cos(angle)
+ sin_a = torch.sin(angle)
+ rotated_x = coords_bhwc[:, :, :, 0] * cos_a - coords_bhwc[:, :, :, 1] * sin_a
+ rotated_y = coords_bhwc[:, :, :, 0] * sin_a + coords_bhwc[:, :, :, 1] * cos_a
+
+ zigzag1 = torch.abs(2.0 * torch.fmod(rotated_x * freq - time_tensor_05, 1.0) - 1.0)
+ zigzag2 = torch.abs(2.0 * torch.fmod(rotated_y * freq + time_tensor_03, 1.0) - 1.0)
+
+ zigzag = torch.minimum(zigzag1, zigzag2)
+ thickness = 0.3 + 0.1 * torch.sin(time_tensor)
+ shape_mask = torch.heaviside(zigzag - thickness, torch.tensor(0.5, device=device)) # step function in torch
+ elif shape_type == "gradient_x": # Keep original implementations if needed
+ shape_mask = coords_bhwc[:, :, :, 0]
+ elif shape_type == "gradient_y":
+ shape_mask = coords_bhwc[:, :, :, 1]
+
+
+ if shape_mask is not None:
+ # Ensure mask is [B, H, W, 1] for broadcasting
+ if len(shape_mask.shape) == 2: # Shape is [H, W]
+ shape_mask = shape_mask.unsqueeze(0).unsqueeze(-1) # Add batch and channel -> [1, H, W, 1]
+ if batch_size > 1:
+ shape_mask = shape_mask.expand(batch_size, height, width, 1) # Expand batch dim
+ elif len(shape_mask.shape) == 3: # Shape is [B, H, W]
+ shape_mask = shape_mask.unsqueeze(-1) # Add channel -> [B, H, W, 1]
+ elif len(shape_mask.shape) == 4 and shape_mask.shape[-1] != 1: # Shape is [B, H, W, C] where C!=1
+ # This case shouldn't happen based on current mask logic, but handle defensively
+ print(f"Warning: Unexpected shape mask shape {shape_mask.shape}. Taking first channel.")
+ shape_mask = shape_mask[..., 0:1] # Take first channel -> [B, H, W, 1]
+ # If shape is already [B, H, W, 1], do nothing
+
+ # Track shape mask tensor (now guaranteed to be 4D)
+ # debugger.track_tensor_shape_history(shape_mask, "shape_mask", "after_creation_and_reshape")
+
+ # Apply shape mask using lerp to the *warped* velocity
+ velocity = torch.lerp(velocity, velocity * shape_mask, shape_mask_strength)
+ # debugger.track_tensor_shape_history(velocity, "velocity_masked", "after_shape_mask_lerp")
+
+
+ # print(f"Applied shape mask '{shape_type}' with strength {shape_mask_strength}")
+
+ # --- Prepare Warped Velocity for Channel Generation ---
+ # Convert *warped* velocity from [B,H,W,2] to [B,2,H,W] for PyTorch processing
+ velocity_bchw = velocity.permute(0, 3, 1, 2)
+ # debugger.track_tensor_shape_history(velocity_bchw, "velocity_bchw", "after_permutation_of_warped")
+
+ # Check if velocity field has the correct shape with 2 channels (X and Y components)
+ velocity_shape = velocity_bchw.shape
+ # print(f"Warped velocity field shape (BCHW): {velocity_shape}")
+
+ # If velocity only has 1 channel instead of 2, fix it
+ if velocity_shape[1] == 1:
+ # print("WARNING: Warped velocity field has only 1 channel, expanding to 2 channels")
+ # Duplicate the channel to create an X and Y component
+ velocity_bchw = velocity_bchw.repeat(1, 2, 1, 1)
+ # Apply a small rotation to the second channel to make it different
+ velocity_bchw[:, 1] = velocity_bchw[:, 0] * 0.8
+ # debugger.track_tensor_shape_history(velocity_bchw, "velocity_bchw", "after_shape_correction")
+
+ # Extract velocity components (vx, vy) from the *warped* velocity field
+ vx = velocity_bchw[:, 0:1] # X component with shape [batch, 1, height, width]
+ vy = velocity_bchw[:, 1:2] # Y component with shape [batch, 1, height, width]
+ # debugger.track_tensor_shape_history(vx, "vx_warped", "warped_velocity_component")
+ # debugger.track_tensor_shape_history(vy, "vy_warped", "warped_velocity_component")
+
+ # --- Generate Base Noise Pattern via Advection ---
+ # Calculate dt for advection based on phase_shift
+ dt = 0.2 + phase_shift * 1.8
+
+ # Perform advection using the *warped* velocity (BHWC format) and scaled coordinates (BHWC format)
+ # The advect function expects velocity in BHWC format [B, H, W, 2]
+ advected_noise_pattern = CurlNoiseGenerator.advect(
+ p * scale, # Use scaled coordinates for advection sampling
+ velocity, # Use the warped velocity (BHWC format)
+ time,
+ dt,
+ octaves,
+ scale, # Pass scale for noise sampling inside advect
+ device,
+ base_seed, # Use base_seed for consistent advection pattern across frames if temporal
+ use_temporal_coherence
+ )
+ # advected_noise_pattern is expected to be [B, H, W, 1]
+ # debugger.track_tensor_shape_history(advected_noise_pattern, "advected_noise_pattern", "after_advect")
+
+ # Convert advected pattern to B,C,H,W format [B, 1, H, W]
+ advected_noise_bchw = advected_noise_pattern.permute(0, 3, 1, 2)
+ # debugger.track_tensor_shape_history(advected_noise_bchw, "advected_noise_bchw", "after_advect_permutation")
+
+ # Determine how many channels to generate
+ channels_to_generate = target_channels # Use all the channels the model requires
+
+ # --- Channel Generation Logic ---
+ # Apply color scheme if specified, using warped velocity components (vx, vy)
+ if color_scheme not in ["none", "0"] and color_intensity > 0 and channels_to_generate >= 3:
+ # Apply color transformations based on the *warped* velocity field's magnitude and direction
+ vmag = torch.sqrt(vx**2 + vy**2) # Magnitude of warped velocity
+
+ # Use a more robust normalization approach for vmag
+ flat_vmag = vmag.view(vmag.shape[0], -1) # [batch, all_other_dims]
+ max_vals = flat_vmag.max(dim=1, keepdim=True)[0] # Get max per batch
+ vmag = vmag / (max_vals.view(-1, 1, 1) + 1e-8) # Reshape to broadcast correctly
+
+ # --- FIX START ---
+ # Ensure vmag used for color interpolation has only 1 channel
+ # This addresses the potential shape mismatch [B, C, ...] vs [1, 3, ...] in lerp
+ # where C might be > 1 (e.g., 16) due to batch size or other factors.
+ vmag_interp = vmag
+ if vmag.shape[1] != 1:
+ # print(f"WARNING: vmag has shape {vmag.shape} before color interpolation. Taking first channel.")
+ vmag_interp = vmag[:, 0:1] # Shape [B, 1, H, W]
+ # --- FIX END ---
+
+ # Track component tensors
+ # debugger.track_tensor_shape_history(vx, "vx_component", "velocity_component")
+ # debugger.track_tensor_shape_history(vy, "vy_component", "velocity_component")
+ # Log the original vmag shape and the one used for interpolation
+ # debugger.track_tensor_shape_history(vmag, "vmag_original", "velocity_component_original")
+ # debugger.track_tensor_shape_history(vmag_interp, "vmag_interp", "velocity_component_for_interp")
+
+ # Compute angle from X axis (normalized to [0,1])
+ vangle = torch.atan2(vy, vx)
+ vangle = (vangle + math.pi) / (2 * math.pi) # Normalize to [0, 1]
+
+ # Ensure angle is [B, 1, H, W] for consistency
+ if len(vangle.shape) == 3: # [B, H, W]
+ vangle = vangle.unsqueeze(1)
+ elif len(vangle.shape) == 4 and vangle.shape[1] != 1: # [B, C, H, W] with C!=1
+ vangle = vangle[:, 0:1, :, :] # Take first channel
+ # Already [B, 1, H, W] requires no change
+
+ # Ensure time is a tensor for calculations
+ time_tensor = torch.tensor(time, device=device, dtype=vx.dtype)
+
+ # Create basic RGB mapping of velocity
+ # Ensure channels have 4 dimensions [B, 1, H, W] before use
+ r_channel = vx # Already [B, 1, H, W]
+ g_channel = vy # Already [B, 1, H, W]
+ b_channel = vmag_interp # Use the single-channel version for direct blue mapping
+
+ # Helper for HSV to RGB
+ def hsv_to_rgb(h, s, v):
+ # h, s, v expected shapes [B, 1, H, W]
+ c = v * s
+ h_prime = h * 6.0
+ x = c * (1.0 - torch.abs(torch.fmod(h_prime, 2.0) - 1.0))
+ m = v - c
+
+ r, g, b = torch.zeros_like(h), torch.zeros_like(h), torch.zeros_like(h)
+
+ mask0 = (h_prime < 1.0)
+ r[mask0], g[mask0], b[mask0] = c[mask0], x[mask0], 0.0
+ mask1 = (h_prime >= 1.0) & (h_prime < 2.0)
+ r[mask1], g[mask1], b[mask1] = x[mask1], c[mask1], 0.0
+ mask2 = (h_prime >= 2.0) & (h_prime < 3.0)
+ r[mask2], g[mask2], b[mask2] = 0.0, c[mask2], x[mask2]
+ mask3 = (h_prime >= 3.0) & (h_prime < 4.0)
+ r[mask3], g[mask3], b[mask3] = 0.0, x[mask3], c[mask3]
+ mask4 = (h_prime >= 4.0) & (h_prime < 5.0)
+ r[mask4], g[mask4], b[mask4] = x[mask4], 0.0, c[mask4]
+ mask5 = (h_prime >= 5.0)
+ r[mask5], g[mask5], b[mask5] = c[mask5], 0.0, x[mask5]
+
+ r, g, b = r + m, g + m, b + m
+ return r, g, b
+
+ # Helper for lerp
+ def lerp(a, b, t):
+ return a + (b - a) * t
+
+ # Helper for color stops interpolation
+ def interpolate_colors(stops, t):
+ # stops: list of [value, color_tensor]
+ # t: normalized value tensor [B, 1, H, W]
+
+ # Find the two stops t falls between
+ idx = torch.zeros_like(t, dtype=torch.long)
+ for i in range(len(stops) - 1):
+ idx = torch.where((t >= stops[i][0]) & (t < stops[i+1][0]), torch.full_like(idx, i), idx)
+ idx = torch.where(t >= stops[-1][0], torch.full_like(idx, len(stops) - 2), idx) # Handle >= last stop value
+
+ # Gather the start and end stops based on idx
+ # Need to manually index based on the calculated 'idx'
+ # This is complex with tensors, so we'll use masks for each segment
+ final_color = torch.zeros_like(stops[0][1].expand(-1, -1, t.shape[2], t.shape[3])) # Initialize with target spatial shape
+
+ for i in range(len(stops) - 1):
+ mask = (idx == i) # Shape [B, 1, H, W]
+ t0, c0 = stops[i] # c0 shape [1, 3, 1, 1]
+ t1, c1 = stops[i+1] # c1 shape [1, 3, 1, 1]
+
+ # Normalize t within the segment [t0, t1] -> [0, 1] for all pixels
+ # Avoid division by zero for constant segments
+ denominator = (t1 - t0 + 1e-8)
+ local_t_all = (t - t0) / denominator
+ local_t_clamped = torch.clamp(local_t_all, 0.0, 1.0) # Shape [B, 1, H, W]
+
+ # Lerp the colors for this segment - c0/c1 will broadcast
+ interp_color = lerp(c0, c1, local_t_clamped) # Shape [B, 3, H, W]
+
+ # Apply the interpolated color where the mask is true
+ # Expand mask to match color channels
+ final_color = torch.where(mask.expand_as(interp_color), interp_color, final_color)
+
+ return final_color[:, 0:1], final_color[:, 1:2], final_color[:, 2:3] # R, G, B
+
+ # Ensure input is normalized [0, 1] for color stops
+ normalized_value = vmag_interp # Use the fixed vmag for all color calculations
+
+ # Different coloring options
+ if color_scheme == "rainbow":
+ # Rainbow coloring based on angle
+ h = vangle # Hue from angle [0, 1]
+ s = torch.ones_like(h) * 0.8 # Fixed saturation
+ v = normalized_value # Value from magnitude [0, 1] # Use fixed value
+ result_r, result_g, result_b = hsv_to_rgb(h, s, v)
+
+ elif color_scheme == "heatmap":
+ # Heatmap coloring based on magnitude
+ result_r = torch.pow(normalized_value, 0.5) # Use fixed value
+ result_g = torch.pow(normalized_value, 1.5) # Use fixed value
+ result_b = torch.pow(normalized_value, 3.0) # Use fixed value
+
+ elif color_scheme == "plasma":
+ # Plasma-like coloring based on angle and magnitude
+ # Ensure time_tensor has compatible shape [B, 1, 1, 1] for broadcasting
+ time_broadcast = time_tensor.view(-1, 1, 1, 1) if time_tensor.numel() > 1 else time_tensor
+
+ result_r = 0.5 + 0.5 * torch.sin(vangle * 6.28318 + time_broadcast)
+ result_g = 0.5 + 0.5 * torch.sin(vangle * 6.28318 + normalized_value * 3.14159 + time_broadcast * 2.0) # Use fixed value
+ result_b = 0.5 + 0.5 * torch.cos(vangle * 3.14159 + normalized_value * 6.28318 + time_broadcast * 3.0) # Use fixed value
+ elif color_scheme == "vorticity":
+ # Vorticity visualization - more accurate curl representation
+ # Calculate numerical curl: dVx/dy - dVy/dx
+ # We need gradient computation here. Using a simpler approximation for now:
+ # curl_approx = (vx shifted up - vx shifted down) - (vy shifted right - vy shifted left)
+ # This requires padding or careful indexing.
+ # Alternative: Use vx - vy as a proxy for rotational direction
+
+ curl_magnitude = torch.abs(vx - vy)
+
+ # Normalize for visualization - using robust approach
+ flat_curl = curl_magnitude.view(curl_magnitude.shape[0], -1)
+ max_vals = flat_curl.max(dim=1, keepdim=True)[0]
+ curl_norm = curl_magnitude / (max_vals.view(-1, 1, 1, 1) + 1e-8)
+
+ # Use a blue-white-red color scheme to show positive/negative vorticity proxy
+ positive_curl_mask = (vx > vy).float()
+ negative_curl_mask = (vx <= vy).float()
+
+ result_r = positive_curl_mask * curl_norm # Red for positive curl proxy
+ result_g = (positive_curl_mask + negative_curl_mask) * (1.0 - curl_norm) # White in middle
+ result_b = negative_curl_mask * curl_norm # Blue for negative curl proxy
+
+ # --- Add other color schemes from JS ---
+ elif color_scheme == "blue_red":
+ c0 = torch.tensor([0.0, 0.0, 1.0], device=device).view(1, 3, 1, 1)
+ c1 = torch.tensor([1.0, 0.0, 0.0], device=device).view(1, 3, 1, 1)
+ color = lerp(c0, c1, normalized_value) # Use fixed value
+ result_r, result_g, result_b = color[:, 0:1], color[:, 1:2], color[:, 2:3]
+ elif color_scheme == "viridis":
+ stops = [
+ (0.0, torch.tensor([0.267, 0.005, 0.329], device=device).view(1, 3, 1, 1)), # #440154
+ (0.33, torch.tensor([0.188, 0.407, 0.553], device=device).view(1, 3, 1, 1)), # #30678D
+ (0.66, torch.tensor([0.208, 0.718, 0.471], device=device).view(1, 3, 1, 1)), # #35B778
+ (1.0, torch.tensor([0.992, 0.906, 0.143], device=device).view(1, 3, 1, 1)) # #FDE724
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ # Plasma already implemented above
+ elif color_scheme == "inferno":
+ stops = [
+ (0.0, torch.tensor([0.001, 0.001, 0.016], device=device).view(1, 3, 1, 1)), # #000004
+ (0.25, torch.tensor([0.259, 0.039, 0.408], device=device).view(1, 3, 1, 1)), # #420A68
+ (0.5, torch.tensor([0.576, 0.149, 0.404], device=device).view(1, 3, 1, 1)), # #932667
+ (0.75, torch.tensor([0.867, 0.318, 0.227], device=device).view(1, 3, 1, 1)), # #DD513A
+ (0.85, torch.tensor([0.988, 0.647, 0.039], device=device).view(1, 3, 1, 1)), # #FCA50A
+ (1.0, torch.tensor([0.988, 1.000, 0.643], device=device).view(1, 3, 1, 1)) # #FCFFA4
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "magma":
+ stops = [
+ (0.0, torch.tensor([0.001, 0.001, 0.016], device=device).view(1, 3, 1, 1)), # #000004
+ (0.25, torch.tensor([0.231, 0.059, 0.439], device=device).view(1, 3, 1, 1)), # #3B0F70
+ (0.5, torch.tensor([0.549, 0.161, 0.506], device=device).view(1, 3, 1, 1)), # #8C2981
+ (0.75, torch.tensor([0.871, 0.288, 0.408], device=device).view(1, 3, 1, 1)), # #DE4968
+ (0.85, torch.tensor([0.996, 0.624, 0.427], device=device).view(1, 3, 1, 1)), # #FE9F6D
+ (1.0, torch.tensor([0.988, 0.992, 0.749], device=device).view(1, 3, 1, 1)) # #FCFDBF
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "turbo":
+ stops = [
+ (0.0, torch.tensor([0.188, 0.071, 0.235], device=device).view(1, 3, 1, 1)), # #30123b
+ (0.25, torch.tensor([0.275, 0.408, 0.859], device=device).view(1, 3, 1, 1)), # #4669db
+ (0.5, torch.tensor([0.149, 0.749, 0.549], device=device).view(1, 3, 1, 1)), # #26bf8c
+ (0.65, torch.tensor([0.831, 1.000, 0.314], device=device).view(1, 3, 1, 1)), # #d4ff50
+ (0.85, torch.tensor([0.980, 0.718, 0.298], device=device).view(1, 3, 1, 1)), # #fab74c
+ (1.0, torch.tensor([0.729, 0.004, 0.000], device=device).view(1, 3, 1, 1)) # #ba0100
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "jet":
+ stops = [
+ (0.0, torch.tensor([0.000, 0.000, 0.498], device=device).view(1, 3, 1, 1)), # #00007f
+ (0.125, torch.tensor([0.000, 0.000, 1.000], device=device).view(1, 3, 1, 1)), # #0000ff blue
+ (0.375, torch.tensor([0.000, 1.000, 1.000], device=device).view(1, 3, 1, 1)), # #00ffff cyan
+ (0.625, torch.tensor([1.000, 1.000, 0.000], device=device).view(1, 3, 1, 1)), # #ffff00 yellow
+ (0.875, torch.tensor([1.000, 0.000, 0.000], device=device).view(1, 3, 1, 1)), # #ff0000 red
+ (1.0, torch.tensor([0.498, 0.000, 0.000], device=device).view(1, 3, 1, 1)) # #7f0000 dark red
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ # Rainbow already implemented above
+ elif color_scheme == "cool":
+ c0 = torch.tensor([0.0, 1.0, 1.0], device=device).view(1, 3, 1, 1) # Cyan
+ c1 = torch.tensor([1.0, 0.0, 1.0], device=device).view(1, 3, 1, 1) # Magenta
+ color = lerp(c0, c1, normalized_value) # Use fixed value
+ result_r, result_g, result_b = color[:, 0:1], color[:, 1:2], color[:, 2:3]
+ elif color_scheme == "hot":
+ stops = [
+ (0.0, torch.tensor([0.0, 0.0, 0.0], device=device).view(1, 3, 1, 1)), # Black
+ (0.375, torch.tensor([1.0, 0.0, 0.0], device=device).view(1, 3, 1, 1)), # Red
+ (0.75, torch.tensor([1.0, 1.0, 0.0], device=device).view(1, 3, 1, 1)), # Yellow
+ (1.0, torch.tensor([1.0, 1.0, 1.0], device=device).view(1, 3, 1, 1)) # White
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "parula":
+ stops = [
+ (0.0, torch.tensor([0.208, 0.165, 0.529], device=device).view(1, 3, 1, 1)), # #352a87
+ (0.25, torch.tensor([0.059, 0.361, 0.867], device=device).view(1, 3, 1, 1)), # #0f5cdd
+ (0.5, torch.tensor([0.000, 0.710, 0.651], device=device).view(1, 3, 1, 1)), # #00b5a6
+ (0.75, torch.tensor([1.000, 0.765, 0.216], device=device).view(1, 3, 1, 1)), # #ffc337
+ (1.0, torch.tensor([0.988, 0.996, 0.643], device=device).view(1, 3, 1, 1)) # #fcfea4
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "hsv":
+ # Use angle for hue, magnitude for value
+ h = vangle
+ s = torch.ones_like(h) * 0.9 # High saturation
+ v = normalized_value # Use fixed value
+ result_r, result_g, result_b = hsv_to_rgb(h, s, v)
+ elif color_scheme == "autumn":
+ c0 = torch.tensor([1.0, 0.0, 0.0], device=device).view(1, 3, 1, 1) # Red
+ c1 = torch.tensor([1.0, 1.0, 0.0], device=device).view(1, 3, 1, 1) # Yellow
+ color = lerp(c0, c1, normalized_value) # Use fixed value
+ result_r, result_g, result_b = color[:, 0:1], color[:, 1:2], color[:, 2:3]
+ elif color_scheme == "winter":
+ c0 = torch.tensor([0.0, 0.0, 1.0], device=device).view(1, 3, 1, 1) # Blue
+ c1 = torch.tensor([0.0, 1.0, 0.5], device=device).view(1, 3, 1, 1) # Greenish-Cyan
+ color = lerp(c0, c1, normalized_value) # Use fixed value
+ result_r, result_g, result_b = color[:, 0:1], color[:, 1:2], color[:, 2:3]
+ elif color_scheme == "spring":
+ c0 = torch.tensor([1.0, 0.0, 1.0], device=device).view(1, 3, 1, 1) # Magenta
+ c1 = torch.tensor([1.0, 1.0, 0.0], device=device).view(1, 3, 1, 1) # Yellow
+ color = lerp(c0, c1, normalized_value) # Use fixed value
+ result_r, result_g, result_b = color[:, 0:1], color[:, 1:2], color[:, 2:3]
+ elif color_scheme == "summer":
+ c0 = torch.tensor([0.0, 0.5, 0.4], device=device).view(1, 3, 1, 1) # Dark Green
+ c1 = torch.tensor([1.0, 1.0, 0.4], device=device).view(1, 3, 1, 1) # Yellow
+ color = lerp(c0, c1, normalized_value) # Use fixed value
+ result_r, result_g, result_b = color[:, 0:1], color[:, 1:2], color[:, 2:3]
+ elif color_scheme == "copper":
+ c0 = torch.tensor([0.0, 0.0, 0.0], device=device).view(1, 3, 1, 1) # Black
+ c1 = torch.tensor([1.0, 0.6, 0.4], device=device).view(1, 3, 1, 1) # Copper
+ color = lerp(c0, c1, normalized_value) # Use fixed value
+ result_r, result_g, result_b = color[:, 0:1], color[:, 1:2], color[:, 2:3]
+ elif color_scheme == "pink":
+ stops = [
+ (0.0, torch.tensor([0.05, 0.05, 0.05], device=device).view(1, 3, 1, 1)), # Dark gray
+ (0.5, torch.tensor([1.0, 0.0, 1.0], device=device).view(1, 3, 1, 1)), # Magenta
+ (1.0, torch.tensor([1.0, 1.0, 1.0], device=device).view(1, 3, 1, 1)) # White
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "bone":
+ stops = [
+ (0.0, torch.tensor([0.0, 0.0, 0.0], device=device).view(1, 3, 1, 1)), # Black
+ (0.375, torch.tensor([0.329, 0.329, 0.455], device=device).view(1, 3, 1, 1)), # Dark blueish-gray
+ (0.75, torch.tensor([0.627, 0.757, 0.757], device=device).view(1, 3, 1, 1)), # Light blueish-gray
+ (1.0, torch.tensor([1.0, 1.0, 1.0], device=device).view(1, 3, 1, 1)) # White
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "ocean":
+ stops = [
+ (0.0, torch.tensor([0.0, 0.0, 0.0], device=device).view(1, 3, 1, 1)), # Black
+ (0.33, torch.tensor([0.0, 0.0, 0.6], device=device).view(1, 3, 1, 1)), # Dark Blue
+ (0.66, torch.tensor([0.0, 0.6, 1.0], device=device).view(1, 3, 1, 1)), # Light Blue
+ (1.0, torch.tensor([0.6, 1.0, 1.0], device=device).view(1, 3, 1, 1)) # Cyan
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "terrain":
+ stops = [
+ (0.0, torch.tensor([0.2, 0.2, 0.6], device=device).view(1, 3, 1, 1)), # Blue
+ (0.33, torch.tensor([0.0, 0.8, 0.4], device=device).view(1, 3, 1, 1)), # Green
+ (0.66, torch.tensor([1.0, 0.8, 0.0], device=device).view(1, 3, 1, 1)), # Yellow
+ (1.0, torch.tensor([1.0, 1.0, 1.0], device=device).view(1, 3, 1, 1)) # White
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "neon":
+ stops = [
+ (0.0, torch.tensor([1.0, 0.0, 1.0], device=device).view(1, 3, 1, 1)), # Magenta
+ (0.5, torch.tensor([0.0, 1.0, 1.0], device=device).view(1, 3, 1, 1)), # Cyan
+ (1.0, torch.tensor([1.0, 1.0, 0.0], device=device).view(1, 3, 1, 1)) # Yellow
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ elif color_scheme == "fire":
+ stops = [
+ (0.0, torch.tensor([0.0, 0.0, 0.0], device=device).view(1, 3, 1, 1)), # Black
+ (0.25, torch.tensor([1.0, 0.0, 0.0], device=device).view(1, 3, 1, 1)), # Red
+ (0.6, torch.tensor([1.0, 1.0, 0.0], device=device).view(1, 3, 1, 1)), # Yellow
+ (1.0, torch.tensor([1.0, 1.0, 1.0], device=device).view(1, 3, 1, 1)) # White
+ ]
+ result_r, result_g, result_b = interpolate_colors(stops, normalized_value) # Use fixed value
+ # --- End of added color schemes ---
+ else:
+ # Default color scheme based on velocity components if unknown
+ # print(f"Warning: Unknown color scheme '{color_scheme}', using default.")
+ result_r = r_channel
+ result_g = g_channel
+ result_b = b_channel
+
+ # Apply color intensity parameter
+ if color_intensity < 1.0:
+ # Interpolate between grayscale and colored
+ grayscale = (result_r + result_g + result_b) / 3.0
+ result_r = lerp(grayscale, result_r, color_intensity)
+ result_g = lerp(grayscale, result_g, color_intensity)
+ result_b = lerp(grayscale, result_b, color_intensity)
+
+ # Collect first 3 channels based on color scheme
+ channels = []
+ if channels_to_generate >= 1:
+ channels.append(result_r)
+ if channels_to_generate >= 2:
+ channels.append(result_g)
+ if channels_to_generate >= 3:
+ channels.append(result_b)
+
+ # Initial result with color channels
+ result = torch.cat(channels, dim=1)
+
+ # If we need more than 3 channels, generate structured variations
+ if target_channels > 3:
+ # print(f"Generating all {target_channels} channels with color scheme '{color_scheme}'")
+ # Generate additional structured channels
+ additional_channels = []
+
+ for c in range(3, target_channels):
+ # Create a controlled variation seed
+ variation_seed = base_seed + 600 + (c * 100)
+ torch.manual_seed(variation_seed)
+
+ # Use slight parameter variations for controlled diversity
+ time_offset = c * 0.1 # Larger offset for more variation with color
+
+ # Create perturbed coordinates with controlled variations
+ c_p = p.clone()
+ phase_angle = torch.tensor([c * 0.2, c * 0.3], device=device).reshape(1, 1, 1, 2)
+ c_p += torch.sin(phase_angle) * 0.1
+
+ # Generate a new velocity field with varied parameters
+ chan_velocity = CurlNoiseGenerator.get_velocity_field(
+ c_p,
+ time + time_offset,
+ octaves * (1.0 + 0.05 * (c-2)), # Slightly different octaves
+ device,
+ variation_seed,
+ use_temporal_coherence
+ )
+
+ # Apply same shape mask if applicable
+ if shape_type not in ["none", "0"] and shape_mask_strength > 0 and shape_mask is not None:
+ # chan_velocity = chan_velocity * (1.0 - shape_factor) + (chan_velocity * shape_mask) * shape_factor # Original line with error
+ # Use lerp for safer application, shape_mask should be [B, H, W, 1]
+ # Ensure chan_velocity is also [B, H, W, 2] before lerp
+ if chan_velocity.shape[-1] == shape_mask.shape[-1]: # Both likely [B,H,W,1]
+ chan_velocity = torch.lerp(chan_velocity, chan_velocity * shape_mask, shape_mask_strength)
+ elif chan_velocity.shape[-1] == 2 and shape_mask.shape[-1] == 1: # Velocity [B,H,W,2], Mask [B,H,W,1]
+ chan_velocity = torch.lerp(chan_velocity, chan_velocity * shape_mask, shape_mask_strength)
+ else:
+ # print(f"Warning: Skipping shape mask application for channel {c} due to incompatible shapes: velocity {chan_velocity.shape}, mask {shape_mask.shape}")
+ pass # Removed print
+
+ # Create color-aware variation based on channel number
+ if c == 3:
+ # Alpha channel - use average of velocity magnitude
+ velo_mag = torch.sqrt(chan_velocity[:,:,:,0]**2 + chan_velocity[:,:,:,1]**2)
+ extra_channel = velo_mag.unsqueeze(-1).permute(0, 3, 1, 2)
+ else:
+ # Extra channels - mix components with controlled weights
+ component1 = chan_velocity[:,:,:,0].unsqueeze(-1)
+ component2 = chan_velocity[:,:,:,1].unsqueeze(-1)
+
+ # Create a unique mix based on channel number
+ mix_ratio = (c * 0.2) % 1.0
+ mixed = component1 * mix_ratio + component2 * (1.0 - mix_ratio)
+ extra_channel = mixed.permute(0, 3, 1, 2)
+
+ # Normalize for consistency
+ extra_channel = (extra_channel - extra_channel.mean()) / (extra_channel.std() + 1e-8)
+
+ additional_channels.append(extra_channel)
+
+ # Combine with original color channels
+ if additional_channels:
+ extra_tensor = torch.cat(additional_channels, dim=1)
+ result = torch.cat([result, extra_tensor], dim=1)
+
+ # print(f"Generated all {result.shape[1]} channels with color scheme '{color_scheme}'")
+ else:
+ # If no color scheme, apply structured variations for all channels
+ vx = velocity_bchw[:, 0:1] # X component [batch, 1, height, width]
+ vy = velocity_bchw[:, 1:2] # Y component [batch, 1, height, width]
+
+ # Compute magnitude for third channel
+ vmag = torch.sqrt(vx**2 + vy**2)
+
+ # Safe normalization that handles empty dimensions
+ try:
+ # Try the direct cascaded max approach first
+ max_val = vmag.max(dim=1, keepdim=True)[0].max(dim=2, keepdim=True)[0].max(dim=3, keepdim=True)[0]
+ vmag = vmag / (max_val + 1e-8)
+ except IndexError:
+ # Fall back to a safer approach if dimensions don't match expectations
+ # print(f"Warning: Using fallback normalization for vmag with shape {vmag.shape}")
+ if vmag.numel() > 0: # If tensor has any elements
+ flat_vmag = vmag.reshape(vmag.shape[0], -1) # Flatten all dimensions except batch
+ max_val = flat_vmag.max(dim=1, keepdim=True)[0].reshape(-1, 1, 1, 1) # Proper reshaping
+ vmag = vmag / (max_val + 1e-8)
+ else:
+ # Handle completely empty tensor
+ vmag = torch.ones_like(vmag) # Use 1.0 as safe default
+
+ # Get the fast mode flag
+ fast_high_channel_noise = shader_params.get("fast_high_channel_noise", False)
+ HIGH_CHANNEL_THRESHOLD = 16 # Define threshold for fast mode
+
+ if fast_high_channel_noise and target_channels > HIGH_CHANNEL_THRESHOLD:
+ # print(f"๐ Using FAST mode for {target_channels} channels.")
+ # Generate only the first 4 base channels
+ base_channels_list = []
+ if target_channels >= 1:
+ base_channels_list.append(vx)
+ # debugger.track_tensor_shape_history(base_channels_list[-1], "vx_channel", "channel_creation_fast")
+ if target_channels >= 2:
+ base_channels_list.append(vy)
+ # debugger.track_tensor_shape_history(base_channels_list[-1], "vy_channel", "channel_creation_fast")
+ if target_channels >= 3:
+ base_channels_list.append(vmag)
+ # debugger.track_tensor_shape_history(base_channels_list[-1], "vmag_channel", "channel_creation_fast")
+ if target_channels >= 4:
+ # Generate just one extra channel for variation (like the original c=3 logic)
+ # print("Generating structured variation for channel 3 (fast mode base)")
+ variation_seed = base_seed + 500 + (3 * 100)
+ torch.manual_seed(variation_seed)
+ time_offset = 3 * 0.05
+ c_p = p.clone() + torch.sin(torch.tensor([3 * 0.1, 3 * 0.2], device=device).reshape(1, 1, 1, 2))
+ c_velocity = CurlNoiseGenerator.get_velocity_field(c_p, time + time_offset, octaves + (3 * 0.1), device, variation_seed, use_temporal_coherence)
+
+ # Basic safety check for c_velocity shape
+ if c_velocity.shape[-1] < 1:
+ # print(f"โ ๏ธ Fast mode velocity (c=3) has incorrect shape {c_velocity.shape}, correcting")
+ c_velocity = torch.zeros((*c_velocity.shape[:-1], 1), device=device) # Add at least one component
+
+ if shape_type not in ["none", "0"] and shape_mask_strength > 0 and shape_mask is not None:
+ c_velocity = c_velocity * (1.0 - shape_factor) + (c_velocity * shape_mask) * shape_factor
+
+ component = c_velocity[..., 0:1] # Use X component
+ extra_channel_4 = component.permute(0, 3, 1, 2)
+ extra_channel_4 = (extra_channel_4 - extra_channel_4.mean()) / (extra_channel_4.std() + 1e-8)
+ # debugger.track_tensor_shape_history(extra_channel_4, f"extra_channel_3", "channel_structured_variation_fast")
+ base_channels_list.append(extra_channel_4)
+
+ if not base_channels_list: # Handle cases where target_channels < 1
+ # print("โ ๏ธ Target channels < 1, cannot generate base channels for fast mode.")
+ result = torch.zeros((batch_size, target_channels, height, width), device=device)
+ else:
+ base_channels = torch.cat(base_channels_list, dim=1)
+ num_base = base_channels.shape[1]
+ if num_base == 0: # Avoid division by zero if no channels were added
+ # print("โ ๏ธ No base channels generated, creating zero tensor.")
+ result = torch.zeros((batch_size, target_channels, height, width), device=device)
+ else:
+ num_repeats = math.ceil(target_channels / num_base)
+ # print(f" Repeating {num_base} base channels {num_repeats} times.")
+ # Ensure num_repeats is at least 1
+ num_repeats = max(1, num_repeats)
+ result = torch.cat([base_channels] * num_repeats, dim=1)[:, :target_channels] # Tile and slice
+ # debugger.track_tensor_shape_history(result, "result_fast_mode", "fast_channel_tiling")
+
+ else: # Original slow loop for normal mode or low channel counts
+ # Start with a list of structured channels
+ channels = []
+ # print(f"Generating structured variations for all {target_channels} channels directly (Normal Mode)")
+
+ # First, add the base channels: Advected noise, Warped X, Warped Y, Warped Mag
+ if target_channels >= 1:
+ channels.append(advected_noise_bchw) # Use the computed noise pattern
+ # debugger.track_tensor_shape_history(channels[-1], "advected_noise_channel", "channel_creation")
+ if target_channels >= 2:
+ channels.append(vx) # Warped X velocity component
+ # debugger.track_tensor_shape_history(channels[-1], "vx_channel", "channel_creation")
+ if target_channels >= 3:
+ channels.append(vy) # Warped Y velocity component
+ # debugger.track_tensor_shape_history(channels[-1], "vy_channel", "channel_creation")
+ if target_channels >= 4:
+ channels.append(vmag) # Warped velocity magnitude
+ # debugger.track_tensor_shape_history(channels[-1], "vmag_channel", "channel_creation")
+
+
+ # Then generate the remaining channels up to target_channels (starting from index 4)
+ for c in range(4, target_channels):
+ # print(f"Generating structured variation for channel {c}")
+ # Create a controlled variation seed derived from base_seed
+ variation_seed = base_seed + 500 + (c * 100)
+ torch.manual_seed(variation_seed)
+
+ # Generate a unique velocity field with controlled parameter variations
+ # Use a slight offset in time, coordinates, and other parameters
+ time_offset = c * 0.05 # Small time offset per channel
+
+ # Create slightly perturbed coordinates for this channel
+ c_p = p.clone()
+ # Add small, controlled perturbation (not random)
+ c_p += torch.sin(torch.tensor([c * 0.1, c * 0.2], device=device).reshape(1, 1, 1, 2))
+
+ # Generate a new velocity field with varied parameters
+ c_velocity = CurlNoiseGenerator.get_velocity_field(
+ c_p,
+ time + time_offset, # Slight time offset
+ octaves + (c * 0.1), # Slight octave variation
+ device,
+ variation_seed, # Controlled seed variation
+ use_temporal_coherence
+ )
+
+ # Ensure the velocity field has the correct shape (2 components)
+ if c_velocity.shape[-1] < 2:
+ # print(f"โ ๏ธ Channel {c} velocity has incorrect shape {c_velocity.shape}, correcting")
+ # Create a properly shaped velocity field
+ c_proper_velocity = torch.zeros((*c_velocity.shape[:-1], 2), device=device)
+ # Copy the existing components
+ c_proper_velocity[..., :c_velocity.shape[-1]] = c_velocity
+ # Add variation for missing components
+ if c_velocity.shape[-1] == 1:
+ c_proper_velocity[..., 1] = c_velocity[..., 0] * 0.7 + torch.sin(torch.tensor(c * 0.3, device=device)) * 0.3
+ c_velocity = c_proper_velocity
+
+ # Apply same shape mask if used
+ if shape_type not in ["none", "0"] and shape_mask_strength > 0 and shape_mask is not None:
+ # Apply shape mask using lerp
+ # Ensure mask is broadcastable: [B, H, W, 1]
+ # Ensure c_velocity is [B, H, W, 2]
+ if c_velocity.shape[-1] == 2 and shape_mask.shape[-1] == 1:
+ c_velocity = torch.lerp(c_velocity, c_velocity * shape_mask, shape_mask_strength)
+ else:
+ # print(f"Warning: Skipping shape mask for channel {c} due to shapes: vel={c_velocity.shape}, mask={shape_mask.shape}")
+ pass # Removed print
+
+
+ # Extract a component based on channel number for variety
+ # Use index c-1 because we already added first 4 channels
+ if c < 6: # First few channels use different components or combinations
+ if c == 3:
+ # For variety, use X component
+ component = c_velocity[..., 0:1]
+ elif c == 4:
+ # For variety, use Y component
+ component = c_velocity[..., 1:2]
+ elif c == 5: # c == 5
+ # For variety, use a mix of X and Y
+ mix_ratio = 0.7
+ component = c_velocity[..., 0:1] * mix_ratio + c_velocity[..., 1:2] * (1.0 - mix_ratio)
+ else:
+ # For later channels, alternate between components with unique transformations
+ component_idx = c % 2 # Alternate between x and y components
+ component = c_velocity[..., component_idx:component_idx+1]
+
+ # Apply a unique transformation for variety
+ if c % 3 == 0:
+ # Apply sine transformation
+ component = torch.sin(component * 3.14159)
+ elif c % 3 == 1:
+ # Apply absolute value transformation
+ component = torch.abs(component) * 2.0 - 1.0
+
+ # Convert from [batch, height, width, 1] to [batch, 1, height, width]
+ extra_channel = component.permute(0, 3, 1, 2)
+
+ # Normalize if needed for consistency
+ extra_channel = (extra_channel - extra_channel.mean()) / (extra_channel.std() + 1e-8)
+
+ # Track the extra channel
+ # debugger.track_tensor_shape_history(extra_channel, f"extra_channel_{c}", "channel_structured_variation")
+
+ # Add to our channel list
+ channels.append(extra_channel)
+
+ # Combine all channels into a single tensor
+ if not channels: # Handle case where target_channels < 1
+ # print("โ ๏ธ Target channels < 1, cannot generate channels for normal mode.")
+ result = torch.zeros((batch_size, target_channels, height, width), device=device)
+ else:
+ result = torch.cat(channels, dim=1)
+ # debugger.track_tensor_shape_history(result, "result_all_channels", "all_channels_combined")
+ # print(f"Generated curl noise with all {result.shape[1]} structured channels directly")
+
+ # If we need additional channels beyond what was already generated
+ if target_channels > result.shape[1]:
+ # print(f"โ ๏ธ Generating {target_channels - result.shape[1]} additional channels for target_channels={target_channels}")
+
+ # Create additional channels
+ additional_channels = []
+
+ # Log operation start
+ # debugger.log_generation_operation("start_additional_channels",
+ # {"result": result},
+ # result,
+ # {"target_channels": target_channels,
+ # "current_channels": result.shape[1]})
+
+ # Only generate needed channels to avoid too many
+ for c in range(result.shape[1], target_channels):
+ # Use a custom random seed derived from the base seed
+ c_seed = seed + 100 * c
+ torch.manual_seed(c_seed)
+
+ # Offset the coordinates slightly for variation
+ c_offset = 0.1 * (c - result.shape[1]) # Small offset for each channel
+ c_p = p.clone()
+ c_p += torch.randn_like(c_p) * 0.05 # Add small random offsets
+
+ # Track perturbed coordinates
+ # debugger.track_tensor_shape_history(c_p, f"perturbed_coords_{c}", "channel_generation")
+
+ # Generate a unique velocity field for this channel
+ chan_velocity = CurlNoiseGenerator.get_velocity_field(
+ c_p, time + c_offset, octaves, device, c_seed, use_temporal_coherence
+ )
+
+ # Track channel velocity field
+ # debugger.track_tensor_shape_history(chan_velocity, f"chan_velocity_{c}", "channel_generation")
+
+ # Check shape of velocity field - critical for ensuring correct channels
+ # print(f"Chan velocity shape before extraction: {chan_velocity.shape}")
+
+ # Handle the case where velocity has a shape that would lead to 64 channels
+ # The typical shape should be [batch, height, width, 2] or similar
+ if chan_velocity.shape[1] == 64 and chan_velocity.shape[2] == 64:
+ # print(f"โ ๏ธ Detected potential problematic velocity shape: {chan_velocity.shape}")
+ # We might need to fix the velocity field directly
+ if chan_velocity.shape[3] > 1:
+ # Just extract one component directly for simplicity
+ component = 0 # Use first component (x velocity)
+ chan_simple = chan_velocity[:, :, :, component:component+1]
+ # Skip further processing, go straight to properly formatted tensor
+ chan_result = torch.zeros((batch_size, 1, height, width), device=device)
+ # Fill with the component we extracted
+ for h in range(height):
+ for w in range(width):
+ chan_result[0, 0, h, w] = chan_simple[0, h, w, 0]
+
+ # Skip to tracking step
+ # print(f"โ Fixed velocity field, created tensor with shape: {chan_result.shape}")
+ # debugger.track_tensor_shape_history(chan_result, f"chan_result_pre_permute_{c}", "channel_extraction_fixed")
+ # debugger.track_tensor_shape_history(chan_result, f"chan_result_post_permute_{c}", "channel_permutation_fixed")
+ additional_channels.append(chan_result)
+ continue # Skip the rest of this loop iteration
+
+ # Apply the same shape mask if applicable
+ if shape_type not in ["none", "0"] and shape_mask_strength > 0 and shape_mask is not None:
+ chan_velocity = chan_velocity * (1.0 - shape_factor) + (chan_velocity * shape_mask) * shape_factor
+
+ # Extract a single component to use as the channel - safely check dimension size
+ if chan_velocity.shape[-1] > 1:
+ # If we have at least 2 dimensions, use modulo to alternate
+ chan_component = c % chan_velocity.shape[-1]
+ chan_result = chan_velocity[:, :, :, chan_component:chan_component+1]
+ # print(f"Extracting component {chan_component} from velocity with shape {chan_velocity.shape}")
+ else:
+ # If only one dimension, use it directly
+ chan_result = chan_velocity[:, :, :, 0:1]
+ # Add a small random variation if it's not the first channel
+ torch.manual_seed(seed + c * 100)
+ chan_result = chan_result + torch.randn_like(chan_result) * 0.1
+
+ # Track pre-permute tensor
+ # debugger.track_tensor_shape_history(chan_result, f"chan_result_pre_permute_{c}", "channel_extraction")
+
+ # Convert from [batch, height, width, 1] to [batch, 1, height, width]
+ if chan_result.shape[1] == height and chan_result.shape[2] == width:
+ # Shape is [batch, height, width, 1] - standard case
+ chan_result = chan_result.permute(0, 3, 1, 2)
+ # print(f"Standard permutation: {chan_result.shape}")
+ else:
+ # Non-standard shape, handle more carefully
+ # print(f"Non-standard shape before permutation: {chan_result.shape}")
+
+ # Make sure we have 4 dimensions
+ if len(chan_result.shape) < 4:
+ chan_result = chan_result.unsqueeze(-1)
+
+ # Create a properly shaped tensor directly
+ proper_chan = torch.zeros((batch_size, 1, height, width), device=device)
+
+ # Fill with resized content from chan_result
+ if chan_result.shape[1] == 1 and chan_result.shape[2] == 1:
+ # Single value case
+ proper_chan.fill_(chan_result[0, 0, 0, 0].item())
+ elif chan_result.shape[1] == 1:
+ # Expand height dimension
+ for h in range(height):
+ proper_chan[0, 0, h, :] = F.interpolate(
+ chan_result[0, 0:1, 0:1, :].permute(0, 3, 1, 2),
+ size=(1, width),
+ mode='nearest'
+ )[0, 0]
+ elif chan_result.shape[2] == 1:
+ # Expand width dimension
+ for w in range(width):
+ proper_chan[0, 0, :, w] = F.interpolate(
+ chan_result[0, :, 0:1, 0:1].permute(0, 3, 1, 2),
+ size=(height, 1),
+ mode='nearest'
+ )[0, 0, :, 0]
+ else:
+ # Use interpolation for general resizing
+ resized = F.interpolate(
+ chan_result.permute(0, 3, 1, 2),
+ size=(height, width),
+ mode='bilinear',
+ align_corners=False
+ )
+ proper_chan = resized
+
+ chan_result = proper_chan
+ # print(f"Reshaped to: {chan_result.shape}")
+
+ # Track post-permute tensor
+ # debugger.track_tensor_shape_history(chan_result, f"chan_result_post_permute_{c}", "channel_permutation")
+
+ # Add to our additional channels
+ additional_channels.append(chan_result)
+
+ # Combine original and additional channels if we have any new ones
+ if additional_channels:
+ # Track result pre-concatenation
+ # debugger.track_tensor_shape_history(result, "result_pre_concat", "pre_concatenation")
+
+ # Combine tensors
+ extra_tensor = torch.cat(additional_channels, dim=1)
+ result = torch.cat([result, extra_tensor], dim=1)
+
+ # Track result post-concatenation
+ # debugger.track_tensor_shape_history(result, "result_post_concat", "post_concatenation")
+
+ # Log operation
+ # debugger.log_generation_operation("concatenate_channels",
+ # {"base": result, "additional": extra_tensor},
+ # result)
+
+ # print(f"Added {len(additional_channels)} additional channels - final shape: {result.shape}")
+ else:
+ # print(f"โ Already generated all {target_channels} channels at once - shape: {result.shape}")
+ pass # Removed print
+
+ # Scale final result to [-1, 1] range for the model - ONLY IF NO COLOR SCHEME APPLIED
+ result = result * 2.0 - 1.0
+
+ # Final check to ensure we have the right shape
+ if result.shape[1] != target_channels:
+ # print(f"โ ๏ธ Channel mismatch: Generated {result.shape[1]} but need {target_channels}")
+
+ # Log the issue
+ # debugger.add_warning(f"Channel count mismatch: got {result.shape[1]} but expected {target_channels}",
+ # category="channel_count")
+
+ # Create correctly shaped tensor
+ corrected = torch.zeros((batch_size, target_channels, height, width), device=device)
+
+ # Copy values from existing channels
+ min_channels = min(result.shape[1], target_channels)
+ corrected[:, :min_channels] = result[:, :min_channels]
+
+ # Generate random values for missing channels
+ if result.shape[1] < target_channels:
+ std = result.std()
+ mean = result.mean()
+ for c in range(min_channels, target_channels):
+ # Add variation to each extra channel
+ phase = (c / target_channels) * 2 * 3.14159
+ corrected[:, c] = torch.sin(coords[:, 0] * 5 + phase) * std + mean
+
+ result = corrected
+
+ # Track final corrected tensor
+ # debugger.track_tensor_shape_history(result, "result_final_corrected", "channel_correction")
+
+ # print(f"โ Adjusted curl noise to {target_channels} channels for compatibility")
+
+ # Generate a debug report for this tensor generation process
+ # debugger.visualize_tensor_shapes_report()
+
+ return result
+
+ @staticmethod
+ def fbm_curl_noise(p, scale, warp_strength, phase_shift, octaves, time, device, seed, use_temporal_coherence=False):
+ """
+ Curl noise fbm implementation matching the WebGL version
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ scale: Scale factor for the noise
+ warp_strength: Amount of coordinate warping
+ phase_shift: Controls the advection time step
+ octaves: Number of detail levels and also determines visualization pattern
+ time: Animation time
+ device: Device to use for computation
+ seed: Random seed value
+ use_temporal_coherence: Whether to use 3D noise with time as third dimension
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ # Scale coordinates
+ p_scaled = p * scale
+
+ # Compute velocity field with temporal coherence if enabled
+ velocity = CurlNoiseGenerator.get_velocity_field(
+ p_scaled, time, octaves, device, seed, use_temporal_coherence
+ )
+
+ # Apply warp control to intensify curl
+ velocity = CurlNoiseGenerator.apply_warp_intensity(velocity, warp_strength)
+
+ # Vary the advection time step based on phase shift (between 0.2 and 2.0)
+ dt = 0.2 + phase_shift * 1.8
+
+ # Advect along the curl field - with temporal coherence if enabled
+ result = CurlNoiseGenerator.advect(
+ p_scaled, velocity, time, dt, octaves, scale, device, seed, use_temporal_coherence
+ )
+
+ # Ensure result is in [-1, 1] range
+ result = torch.clamp(result, -1.0, 1.0)
+
+ return result
+
+ @staticmethod
+ def compute_gradient(p, epsilon, seed):
+ """
+ Compute gradient of scalar field
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ epsilon: Small value for finite difference
+ seed: Random seed for noise generation
+
+ Returns:
+ Gradient tensor [batch, height, width, 2]
+ """
+ batch, height, width, _ = p.shape
+ device = p.device
+
+ # Sample the potential field at nearby points
+ p_plus_x = torch.clone(p)
+ p_plus_x[:, :, :, 0] += epsilon
+
+ p_minus_x = torch.clone(p)
+ p_minus_x[:, :, :, 0] -= epsilon
+
+ p_plus_y = torch.clone(p)
+ p_plus_y[:, :, :, 1] += epsilon
+
+ p_minus_y = torch.clone(p)
+ p_minus_y[:, :, :, 1] -= epsilon
+
+ # Get simplex noise at each point
+ n_plus_x = CurlNoiseGenerator.simplex_noise(p_plus_x, seed)
+ n_minus_x = CurlNoiseGenerator.simplex_noise(p_minus_x, seed)
+ n_plus_y = CurlNoiseGenerator.simplex_noise(p_plus_y, seed)
+ n_minus_y = CurlNoiseGenerator.simplex_noise(p_minus_y, seed)
+
+ # Compute the gradient using finite difference
+ dx = (n_plus_x - n_minus_x) / (2.0 * epsilon)
+ dy = (n_plus_y - n_minus_y) / (2.0 * epsilon)
+
+ # Combine into a gradient vector
+ gradient = torch.cat([dx, dy], dim=-1)
+
+ return gradient
+
+ @staticmethod
+ def compute_curl(p, epsilon, seed):
+ """
+ Compute curl of vector field (z component in 2D)
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ epsilon: Small value for finite difference
+ seed: Random seed
+
+ Returns:
+ Curl tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = p.shape
+ device = p.device
+
+ # We'll use two offset perlin noise functions as our potential fields
+ # For 2D curl, we need partial derivatives of two potential fields
+
+ # Sample potential field 1
+ p_plus_x_1 = torch.clone(p)
+ p_plus_x_1[:, :, :, 0] += epsilon
+ p_minus_x_1 = torch.clone(p)
+ p_minus_x_1[:, :, :, 0] -= epsilon
+ p_plus_y_1 = torch.clone(p)
+ p_plus_y_1[:, :, :, 1] += epsilon
+ p_minus_y_1 = torch.clone(p)
+ p_minus_y_1[:, :, :, 1] -= epsilon
+
+ # Sample potential field 2 (with offset for independence)
+ offset = torch.tensor([0.0, 100.0], device=device).reshape(1, 1, 1, 2)
+ p_plus_x_2 = torch.clone(p) + offset
+ p_plus_x_2[:, :, :, 0] += epsilon
+ p_minus_x_2 = torch.clone(p) + offset
+ p_minus_x_2[:, :, :, 0] -= epsilon
+ p_plus_y_2 = torch.clone(p) + offset
+ p_plus_y_2[:, :, :, 1] += epsilon
+ p_minus_y_2 = torch.clone(p) + offset
+ p_minus_y_2[:, :, :, 1] -= epsilon
+
+ # Get simplex noise at each point for both potential fields
+ n_plus_x_1 = CurlNoiseGenerator.simplex_noise(p_plus_x_1, seed)
+ n_minus_x_1 = CurlNoiseGenerator.simplex_noise(p_minus_x_1, seed)
+ n_plus_y_1 = CurlNoiseGenerator.simplex_noise(p_plus_y_1, seed)
+ n_minus_y_1 = CurlNoiseGenerator.simplex_noise(p_minus_y_1, seed)
+
+ n_plus_x_2 = CurlNoiseGenerator.simplex_noise(p_plus_x_2, seed + 1) # Different seed for independence
+ n_minus_x_2 = CurlNoiseGenerator.simplex_noise(p_minus_x_2, seed + 1)
+ n_plus_y_2 = CurlNoiseGenerator.simplex_noise(p_plus_y_2, seed + 1)
+ n_minus_y_2 = CurlNoiseGenerator.simplex_noise(p_minus_y_2, seed + 1)
+
+ # Normalize gradients
+ pot1_dx = (n_plus_x_1 - n_minus_x_1) / (2.0 * epsilon)
+ pot1_dy = (n_plus_y_1 - n_minus_y_1) / (2.0 * epsilon)
+ pot2_dx = (n_plus_x_2 - n_minus_x_2) / (2.0 * epsilon)
+ pot2_dy = (n_plus_y_2 - n_minus_y_2) / (2.0 * epsilon)
+
+ # Compute curl (cross product in 2D: โpot2/โx - โpot1/โy)
+ curl = pot2_dx - pot1_dy
+
+ return curl
+
+ @staticmethod
+ def get_velocity_field(p, time, octaves, device, seed, use_temporal_coherence=False):
+ """
+ Get a fluid velocity field based on curl
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ time: Animation time
+ octaves: Number of octaves to use
+ device: Device to use
+ seed: Random seed
+ use_temporal_coherence: Whether to use 3D noise with time as third dimension
+
+ Returns:
+ Velocity field [batch, height, width, 2]
+ """
+ batch, height, width, _ = p.shape
+
+ # Initialize velocity field
+ velocity = torch.zeros_like(p)
+ epsilon = 0.01
+
+ # Base frequency and amplitude
+ frequency = 1.0
+ amplitude = 1.0
+
+ # Loop through octaves
+ for i in range(min(int(octaves), 3)):
+ # Time-varied position
+ time_offset = torch.tensor([time * 0.1 * frequency, 0.0], device=device).reshape(1, 1, 1, 2)
+ pos = p * frequency + time_offset
+
+ # Compute curl using appropriate method based on temporal coherence setting
+ if use_temporal_coherence:
+ # Create 3D position with time as third dimension
+ # Use a different time offset per octave for varied animation
+ time_scale = 0.3 + i * 0.2 # Different time scale per octave
+ pos_3d = torch.cat([
+ pos,
+ torch.ones_like(pos[:, :, :, :1]) * time * time_scale
+ ], dim=-1)
+
+ # Compute curl using 3D sampling for temporal coherence
+ curl = CurlNoiseGenerator.compute_curl_temporal(pos_3d, epsilon, seed + i)
+
+ # Sample curl at offset positions to compute gradient of curl
+ pos_plus_y = torch.clone(pos_3d)
+ pos_plus_y[:, :, :, 1] += epsilon
+
+ pos_plus_x = torch.clone(pos_3d)
+ pos_plus_x[:, :, :, 0] += epsilon
+
+ curl_plus_y = CurlNoiseGenerator.compute_curl_temporal(pos_plus_y, epsilon, seed + i)
+ curl_plus_x = CurlNoiseGenerator.compute_curl_temporal(pos_plus_x, epsilon, seed + i)
+ else:
+ # Original approach
+ curl = CurlNoiseGenerator.compute_curl(pos, epsilon, seed + i)
+
+ # Sample curl at offset positions to compute gradient of curl
+ pos_plus_y = torch.clone(pos)
+ pos_plus_y[:, :, :, 1] += epsilon
+
+ pos_plus_x = torch.clone(pos)
+ pos_plus_x[:, :, :, 0] += epsilon
+
+ curl_plus_y = CurlNoiseGenerator.compute_curl(pos_plus_y, epsilon, seed + i)
+ curl_plus_x = CurlNoiseGenerator.compute_curl(pos_plus_x, epsilon, seed + i)
+
+ # The gradient of the curl gives us a divergence-free field
+ vel_x = (curl_plus_y - curl) / epsilon
+ vel_y = (curl - curl_plus_x) / epsilon
+
+ # Combine into velocity vector
+ vel = torch.cat([vel_x, vel_y], dim=-1)
+
+ # Add to total velocity with current amplitude
+ velocity = velocity + vel * amplitude
+
+ # Prepare for next octave
+ frequency *= 2.0
+ amplitude *= 0.5
+ epsilon *= 0.5 # Adjust epsilon for higher frequencies
+
+ return velocity
+
+ @staticmethod
+ def advect(p, velocity, time, dt, octaves, scale, device, seed, use_temporal_coherence=False):
+ """
+ Advect a property along the velocity field
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ velocity: Velocity field [batch, height, width, 2]
+ time: Animation time
+ dt: Advection time step
+ octaves: Determines pattern type
+ scale: Scale factor
+ device: Device to use
+ seed: Random seed
+ use_temporal_coherence: Whether to use 3D noise with time as third dimension
+
+ Returns:
+ Advected property [batch, height, width, 1]
+ """
+ batch, height, width, _ = p.shape
+
+ # Trace particle backward in time - apply dt more strongly
+ particle_pos = p - velocity * dt
+
+ # Sample different noise patterns based on octave setting
+ pattern_type = int(octaves) % 4
+
+ if pattern_type == 0:
+ # Classic advected noise
+ # Apply phase shift (via dt) to time offset for more visible effect
+ time_offset = torch.tensor([time * 0.2 * dt, dt * 0.5], device=device).reshape(1, 1, 1, 2)
+
+ if use_temporal_coherence:
+ # For temporal coherence, add time as explicit third dimension
+ particle_pos_3d = torch.cat([
+ particle_pos * scale + time_offset,
+ torch.ones_like(particle_pos[:, :, :, :1]) * time
+ ], dim=-1)
+
+ # Use 3D simplex noise for smooth transitions
+ result = CurlNoiseGenerator.simplex_noise_3d(particle_pos_3d, seed)
+ else:
+ # Original implementation
+ result = CurlNoiseGenerator.simplex_noise(particle_pos * scale + time_offset, seed)
+
+ elif pattern_type == 1:
+ # Dye injection visualization
+ # Calculate distance from center of cell
+ fract_pos = particle_pos - torch.floor(particle_pos)
+ center_offset = fract_pos - 0.5
+
+ # For temporal coherence, smoothly vary the distance calculation
+ if use_temporal_coherence:
+ # Use time to smoothly vary the visualization
+ # Convert time to tensor for torch.sin
+ time_tensor = torch.tensor(time, device=device)
+ time_factor = torch.sin(time_tensor * 2.0 * math.pi) * 0.1 + 1.0
+ dist = torch.norm(center_offset, dim=-1, keepdim=True) * (2.0 - dt * 0.5) * time_factor
+ else:
+ # Original implementation
+ dist = torch.norm(center_offset, dim=-1, keepdim=True) * (2.0 - dt * 0.5)
+
+ # Create spots using smoothstep
+ t = torch.clamp((0.4 - dist) / 0.4, 0.0, 1.0)
+ spots = t * t * (3.0 - 2.0 * t) # Smoothstep implementation
+
+ # Remap to [-1, 1]
+ result = spots * 2.0 - 1.0
+
+ elif pattern_type == 2:
+ # Flow lines visualization
+ # Normalize velocity
+ vel_norm = torch.norm(velocity, dim=-1, keepdim=True)
+ vel_norm = torch.where(vel_norm < 1e-6, torch.ones_like(vel_norm), vel_norm)
+ normalized_vel = velocity / vel_norm
+
+ # Calculate dot product for streak lines
+ dot_product = torch.sum(particle_pos * normalized_vel, dim=-1, keepdim=True)
+
+ if use_temporal_coherence:
+ # Generate flow lines with temporal phase shift
+ # This creates a smooth flowing pattern over time
+ phase = time * dt * 2.0 # Temporal phase shift
+ # Convert phase to tensor for torch.sin
+ phase_tensor = torch.tensor(phase, device=device)
+ result = torch.sin(dot_product * (10.0 + dt * 5.0) + phase_tensor)
+ else:
+ # Original implementation
+ # Convert time to tensor for torch.sin
+ time_dt_tensor = torch.tensor(time * dt, device=device)
+ result = torch.sin(dot_product * (10.0 + dt * 5.0) + time_dt_tensor)
+
+ else: # pattern_type == 3
+ # Vorticity visualization (shows rotations in the flow)
+ # Use dt to affect the scale at which vorticity is computed
+ if use_temporal_coherence:
+ # Create 3D coordinates with time
+ particle_pos_3d = torch.cat([
+ particle_pos * scale * (1.0 + dt * 0.5),
+ torch.ones_like(particle_pos[:, :, :, :1]) * time
+ ], dim=-1)
+
+ # Use temporal coherent curl computation
+ vorticity = CurlNoiseGenerator.compute_curl_temporal(particle_pos_3d, 0.01 / dt, seed)
+ else:
+ # Original implementation
+ vorticity = CurlNoiseGenerator.compute_curl(particle_pos * scale * (1.0 + dt * 0.5), 0.01 / dt, seed)
+
+ # Amplify for better visibility - scale by dt for phase shift effect
+ result = vorticity * (2.0 + dt)
+
+ return result
+
+ @staticmethod
+ def apply_warp_intensity(velocity, warp):
+ """
+ Apply the warp control to intensify curl
+
+ Args:
+ velocity: Velocity field [batch, height, width, 2]
+ warp: Warp intensity
+
+ Returns:
+ Warped velocity field [batch, height, width, 2]
+ """
+ # Calculate vector lengths
+ length = torch.norm(velocity, dim=-1, keepdim=True)
+
+ # Avoid division by zero
+ length = torch.maximum(length, torch.ones_like(length) * 0.001)
+
+ # Normalize velocity
+ normalized_vel = velocity / length
+
+ # Apply non-linear scaling based on warp parameter
+ log_scale = torch.log(length * 9.0 + 1.0) * warp
+
+ # Apply scaling to normalized vectors
+ return normalized_vel * log_scale
+
+ @staticmethod
+ def simplex_noise(coords, seed=0):
+ """
+ Simplex noise implementation in PyTorch
+ Provides noise values similar to snoise in GLSL
+
+ Args:
+ coords: Coordinate tensor [batch, height, width, 2]
+ seed: Random seed value
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, dim = coords.shape
+ device = coords.device
+
+ # Set deterministic seed
+ torch.manual_seed(seed)
+
+ if dim == 2:
+ # Constants
+ F2 = 0.366025404 # (sqrt(3) - 1) / 2
+ G2 = 0.211324865 # (3 - sqrt(3)) / 6
+
+ # Skew the input space to determine which simplex cell we're in
+ s = (coords[:, :, :, 0] + coords[:, :, :, 1]) * F2
+ s = s.unsqueeze(-1)
+
+ i = torch.floor(coords[:, :, :, 0] + s[:, :, :, 0])
+ j = torch.floor(coords[:, :, :, 1] + s[:, :, :, 0])
+
+ i = i.unsqueeze(-1)
+ j = j.unsqueeze(-1)
+
+ # Unskew the cell origin back to (x,y) space
+ t = (i + j) * G2
+ X0 = i - t
+ Y0 = j - t
+
+ # The x,y distances from the cell origin
+ x0 = coords[:, :, :, 0:1] - X0
+ y0 = coords[:, :, :, 1:2] - Y0
+
+ # Determine which simplex we are in
+ i1 = torch.zeros_like(i)
+ j1 = torch.zeros_like(j)
+
+ # Offsets for the second corner of simplex in (i,j) coords
+ i1 = torch.where(x0 > y0, torch.ones_like(i), i1)
+ j1 = torch.where(x0 <= y0, torch.ones_like(j), j1)
+
+ # Offsets for the middle corner in (x,y) unskewed coords
+ x1 = x0 - i1 + G2
+ y1 = y0 - j1 + G2
+ x2 = x0 - 1.0 + 2.0 * G2
+ y2 = y0 - 1.0 + 2.0 * G2
+
+ # Work out the hashed gradient indices of the three simplex corners
+ # Using a simplified hash function that's deterministic yet varied
+ def hash_func(i, j, seed_val=seed):
+ return ((i * 1664525 + j * 1013904223) % 16384) / 16384.0 + seed_val/10.0
+
+ # Get random gradients at corners
+ h00 = hash_func(i, j)
+ h10 = hash_func(i + 1, j)
+ h01 = hash_func(i, j + 1)
+ h11 = hash_func(i + 1, j + 1)
+
+ # Compute gradient directions
+ def get_grad(h):
+ # Convert hash to angle
+ angle = h * math.pi * 2.0
+ # Return unit vector
+ return torch.cat([
+ torch.cos(angle),
+ torch.sin(angle)
+ ], dim=-1)
+
+ # Get gradients at corners
+ g00 = get_grad(h00)
+ g10 = get_grad(h10)
+ g01 = get_grad(h01)
+ g11 = get_grad(h11)
+
+ # Calculate noise contributions from each corner
+ def calculate_corner(x, y, gx, gy):
+ # Calculate t = 0.5 - x^2 - y^2 which is > 0 in the simplex
+ t = 0.5 - x*x - y*y
+ t = torch.maximum(torch.zeros_like(t), t)
+
+ # Calculate the noise value: (t^4) * (gx*x + gy*y)
+ n = t*t*t*t * (gx*x + gy*y)
+ return n
+
+ # Compute noise from all corners
+ n0 = calculate_corner(x0, y0, g00[:, :, :, 0:1], g00[:, :, :, 1:2])
+ n1 = calculate_corner(x1, y1, g10[:, :, :, 0:1] if torch.all(i1 > 0) else g01[:, :, :, 0:1],
+ g10[:, :, :, 1:2] if torch.all(i1 > 0) else g01[:, :, :, 1:2])
+ n2 = calculate_corner(x2, y2, g11[:, :, :, 0:1], g11[:, :, :, 1:2])
+
+ # Sum them up and scale
+ noise = 70.0 * (n0 + n1 + n2)
+
+ # Ensure output is in valid [-1, 1] range
+ noise = torch.clamp(noise, -1.0, 1.0)
+
+ return noise
+ else:
+ # Fallback for non-2D coords
+ # print(f"Warning: simplex_noise implementation only supports 2D coordinates")
+ return torch.zeros(batch, height, width, 1, device=device)
+
+ @staticmethod
+ def compute_curl_temporal(p, epsilon, seed):
+ """
+ Compute curl of vector field (z component in 2D) using 3D coordinates for temporal coherence
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 3] with time as third dimension
+ epsilon: Small value for finite difference
+ seed: Random seed
+
+ Returns:
+ Curl tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = p.shape
+ device = p.device
+
+ # We'll use two offset simplex noise functions as our potential fields
+ # For 2D curl from 3D coordinates, we extract spatial components
+
+ # Sample potential field 1
+ p_plus_x_1 = torch.clone(p)
+ p_plus_x_1[:, :, :, 0] += epsilon
+ p_minus_x_1 = torch.clone(p)
+ p_minus_x_1[:, :, :, 0] -= epsilon
+ p_plus_y_1 = torch.clone(p)
+ p_plus_y_1[:, :, :, 1] += epsilon
+ p_minus_y_1 = torch.clone(p)
+ p_minus_y_1[:, :, :, 1] -= epsilon
+
+ # Sample potential field 2 (with offset for independence)
+ offset = torch.tensor([0.0, 100.0, 50.0], device=device).reshape(1, 1, 1, 3)
+ p_plus_x_2 = torch.clone(p) + offset
+ p_plus_x_2[:, :, :, 0] += epsilon
+ p_minus_x_2 = torch.clone(p) + offset
+ p_minus_x_2[:, :, :, 0] -= epsilon
+ p_plus_y_2 = torch.clone(p) + offset
+ p_plus_y_2[:, :, :, 1] += epsilon
+ p_minus_y_2 = torch.clone(p) + offset
+ p_minus_y_2[:, :, :, 1] -= epsilon
+
+ # Get simplex noise at each point for both potential fields using 3D noise
+ n_plus_x_1 = CurlNoiseGenerator.simplex_noise_3d(p_plus_x_1, seed)
+ n_minus_x_1 = CurlNoiseGenerator.simplex_noise_3d(p_minus_x_1, seed)
+ n_plus_y_1 = CurlNoiseGenerator.simplex_noise_3d(p_plus_y_1, seed)
+ n_minus_y_1 = CurlNoiseGenerator.simplex_noise_3d(p_minus_y_1, seed)
+
+ n_plus_x_2 = CurlNoiseGenerator.simplex_noise_3d(p_plus_x_2, seed + 1) # Different seed for independence
+ n_minus_x_2 = CurlNoiseGenerator.simplex_noise_3d(p_minus_x_2, seed + 1)
+ n_plus_y_2 = CurlNoiseGenerator.simplex_noise_3d(p_plus_y_2, seed + 1)
+ n_minus_y_2 = CurlNoiseGenerator.simplex_noise_3d(p_minus_y_2, seed + 1)
+
+ # Normalize gradients
+ pot1_dx = (n_plus_x_1 - n_minus_x_1) / (2.0 * epsilon)
+ pot1_dy = (n_plus_y_1 - n_minus_y_1) / (2.0 * epsilon)
+ pot2_dx = (n_plus_x_2 - n_minus_x_2) / (2.0 * epsilon)
+ pot2_dy = (n_plus_y_2 - n_minus_y_2) / (2.0 * epsilon)
+
+ # Compute curl (cross product in 2D: โpot2/โx - โpot1/โy)
+ curl = pot2_dx - pot1_dy
+
+ return curl
+
+ @staticmethod
+ def simplex_noise_3d(coords, seed=0):
+ """
+ 3D simplex noise implementation in PyTorch for temporal coherence
+ This provides coherent noise values when the third dimension is time
+
+ Args:
+ coords: Coordinate tensor [batch, height, width, 3] with time as third dimension
+ seed: Random seed value
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = coords.shape
+ device = coords.device
+
+ # Set deterministic seed
+ torch.manual_seed(seed)
+
+ # Extract x, y, z (time) components
+ x, y, z = coords[:, :, :, 0], coords[:, :, :, 1], coords[:, :, :, 2]
+
+ # 3D Simplex noise constants
+ F3 = 1.0 / 3.0
+ G3 = 1.0 / 6.0
+
+ # Skew input space to determine which simplex cell contains point
+ s = (x + y + z) * F3
+ i = torch.floor(x + s)
+ j = torch.floor(y + s)
+ k = torch.floor(z + s)
+
+ t = (i + j + k) * G3
+ X0 = i - t
+ Y0 = j - t
+ Z0 = k - t
+
+ # Unskewed distances from cell origin
+ x0 = x - X0
+ y0 = y - Y0
+ z0 = z - Z0
+
+ # Determine simplex (tetrahedron) based on which of x0, y0, z0 is largest
+ # We need to rank the coordinates to determine which simplex we're in
+ # This is a simplified approach that works for most cases
+ x_ge_y = (x0 >= y0).float()
+ y_ge_z = (y0 >= z0).float()
+ x_ge_z = (x0 >= z0).float()
+
+ # Calculate offsets for second corner
+ i1 = (x_ge_y * x_ge_z).float()
+ j1 = ((1.0 - x_ge_y) * y_ge_z).float()
+ k1 = ((1.0 - x_ge_z) * (1.0 - y_ge_z)).float()
+
+ # Calculate offsets for third corner
+ i2 = (x_ge_y + (1.0 - x_ge_y) * x_ge_z).float()
+ j2 = (x_ge_y * (1.0 - x_ge_z) + (1.0 - x_ge_y)).float()
+ k2 = ((1.0 - x_ge_z) + x_ge_z * (1.0 - x_ge_y)).float()
+
+ # Calculate corner positions (in unskewed coordinates)
+ x1 = x0 - i1 + G3
+ y1 = y0 - j1 + G3
+ z1 = z0 - k1 + G3
+
+ x2 = x0 - i2 + 2.0 * G3
+ y2 = y0 - j2 + 2.0 * G3
+ z2 = z0 - k2 + 2.0 * G3
+
+ x3 = x0 - 1.0 + 3.0 * G3
+ y3 = y0 - 1.0 + 3.0 * G3
+ z3 = z0 - 1.0 + 3.0 * G3
+
+ # Hash coordinates of the corners to get random gradients
+ # This is a simplified hash function that works for visualization
+ def hash_coord(a, b, c, s):
+ h = 1664525 * a + 1013904223
+ h += 1664525 * b + 1013904223
+ h += 1664525 * c + 1013904223
+ h += s # Add seed for variation
+ h = torch.fmod(h, 3777.7777)
+
+ # Map hash to a unit vector (gradient)
+ theta = h * 3.883222 # ~pi*1.236 to cover the unit sphere
+ phi = h * 0.67899 # ~pi/5 for variation
+
+ # Create 3D gradient direction
+ gx = torch.cos(theta) * torch.cos(phi)
+ gy = torch.sin(theta) * torch.cos(phi)
+ gz = torch.sin(phi)
+ return torch.stack([gx, gy, gz], dim=-1)
+
+ # Get gradient vectors for each corner
+ g0 = hash_coord(i, j, k, seed)
+ g1 = hash_coord(i + i1, j + j1, k + k1, seed)
+ g2 = hash_coord(i + i2, j + j2, k + k2, seed)
+ g3 = hash_coord(i + 1.0, j + 1.0, k + 1.0, seed)
+
+ # Calculate noise contribution from each corner
+ def corner_influence(grad, x, y, z):
+ # Distance vector from corner to point
+ t = 0.6 - x*x - y*y - z*z
+ t = torch.maximum(t, torch.zeros_like(t))
+ t4 = t*t*t*t
+
+ # Gradient dot product with vertex->point vector
+ n = grad[:,:,:,0]*x + grad[:,:,:,1]*y + grad[:,:,:,2]*z
+ return t4 * n
+
+ # Compute contributions from the four corners
+ n0 = corner_influence(g0, x0, y0, z0)
+ n1 = corner_influence(g1, x1, y1, z1)
+ n2 = corner_influence(g2, x2, y2, z2)
+ n3 = corner_influence(g3, x3, y3, z3)
+
+ # Sum contributions and scale to match 2D output range
+ noise = (n0 + n1 + n2 + n3) * 30.0
+
+ return noise.unsqueeze(-1)
+
+ @staticmethod
+ def add_temporal_coherent_curl_to_shader_params(animation_frame, animation_length, phase_shift, base_seed, use_temporal_coherence, color_scheme="none", color_intensity=0.8):
+ """
+ Helper function to prepare temporal coherence parameters for curl noise.
+
+ Args:
+ animation_frame: Current frame number in animation
+ animation_length: Total number of frames in animation
+ phase_shift: Phase shift amount
+ base_seed: Base seed for consistent noise across frames
+ use_temporal_coherence: Whether to use temporal coherence
+ color_scheme: Color scheme to use for animation
+ color_intensity: Intensity of color effect
+
+ Returns:
+ Dictionary with calculated time, seed and phase values
+ """
+ if animation_length <= 1:
+ animation_length = 100 # Default for non-animations
+
+ if use_temporal_coherence:
+ # Calculate normalized time (0 to 1 over animation)
+ time = animation_frame / animation_length
+ # Scale to appropriate range for noise
+ time = time * 10.0 # Scale time to make movement more apparent
+ seed = base_seed
+ else:
+ # Without temporal coherence, use frame-dependent seed
+ time = 0.0
+ seed = base_seed + int(animation_frame * 1000)
+
+ # Apply phase shift
+ phase = (animation_frame / animation_length) * phase_shift
+
+ # print(f"Temporal params - time: {time}, seed: {seed}, frame: {animation_frame}, phase: {phase}")
+
+ return {
+ "time": time,
+ "seed": seed,
+ "phase": phase,
+ "colorScheme": color_scheme,
+ "shaderColorIntensity": color_intensity
+ }
+
+def generate_curl_noise_tensor(shader_params, height, width, batch_size=1, device="cuda", seed=0, target_channels=4):
+ """
+ Generate curl noise tensor directly using shader_params
+
+ Args:
+ shader_params: Dictionary containing parameters from shader_params.json
+ height: Height of tensor
+ width: Width of tensor
+ batch_size: Number of images in batch
+ device: Device to create tensor on
+ seed: Random seed to ensure consistent noise with same seed
+ target_channels: Number of channels to generate (default: 4)
+
+ Returns:
+ Noise tensor with shape [batch_size, target_channels, height, width]
+ """
+ # Extract temporal coherence parameters if present
+ time = shader_params.get("time", 0.0)
+ base_seed = shader_params.get("base_seed", seed)
+ use_temporal_coherence = shader_params.get("useTemporalCoherence", shader_params.get("temporal_coherence", False))
+
+ # Extract or use provided target_channels - handle mismatches between parameters and dictionary
+ if "target_channels" in shader_params:
+ target_channels = shader_params["target_channels"]
+ # print(f"Curl noise using target_channels={target_channels} from shader_params") # Redundant print
+ else:
+ shader_params["target_channels"] = target_channels # Add to shader_params
+ # print(f"Setting target_channels={target_channels} in shader_params") # Redundant print
+
+ # Get color parameters
+ color_scheme = shader_params.get("colorScheme", "none")
+ color_intensity = shader_params.get("shaderColorIntensity", shader_params.get("intensity", 0.8))
+
+ # Print target channels for debugging
+ # print(f"Target channels: {target_channels}") # Redundant print
+
+ # Set deterministic seed for this operation
+ torch.manual_seed(base_seed if use_temporal_coherence else seed)
+
+ # --- Removed Redundant Parameter Extraction & Printing ---
+ # The parameters are extracted and printed within get_curl_noise now.
+ # scale = shader_params.get("scale", 1.0)
+ # warp_strength = shader_params.get("warp_strength", 0.5)
+ # phase_shift = shader_params.get("phase_shift", 0.5)
+ # octaves = shader_params.get("octaves", 3.0)
+ # shape_type = shader_params.get("shape_type", "none")
+ # shape_mask_strength = shader_params.get("shapemaskstrength", 1.0)
+
+ # Debug output of important parameters - Removed as it's handled in get_curl_noise
+ # print(f"Curl noise using: scale={scale}, warp={warp_strength}, phase={phase_shift}, octaves={octaves}")
+ # print(f"Temporal settings: time={time}, base_seed={base_seed}, coherence={use_temporal_coherence}")
+ # print(f"Shape mask params: type={shape_type}, strength={shape_mask_strength}")
+ # print(f"Color settings: scheme={color_scheme}, intensity={color_intensity}")
+ # print(f"Target channels: {target_channels}")
+
+ # Generate curl noise using the CurlNoiseGenerator
+ noise_tensor = CurlNoiseGenerator.get_curl_noise(
+ batch_size=batch_size,
+ height=height,
+ width=width,
+ shader_params=shader_params,
+ device=device,
+ seed=base_seed if use_temporal_coherence else seed,
+ target_channels=target_channels
+ )
+
+ # Reset random seed state to not affect other operations
+ torch.manual_seed(torch.seed())
+
+ # Normalize the noise to match typical noise distribution
+ if noise_tensor.numel() > 0:
+ noise_tensor = (noise_tensor - noise_tensor.mean()) / (noise_tensor.std() + 1e-8)
+
+ # Ensure we have the right number of channels for the model
+ if noise_tensor.shape[1] != target_channels:
+ print(f"โ ๏ธ Channel count mismatch: got {noise_tensor.shape[1]}, need {target_channels}")
+ # Create a tensor with the correct number of channels
+ corrected_tensor = torch.zeros((batch_size, target_channels, height, width),
+ device=noise_tensor.device,
+ dtype=noise_tensor.dtype)
+
+ # Copy available channels
+ channels_to_copy = min(noise_tensor.shape[1], target_channels)
+ corrected_tensor[:, :channels_to_copy] = noise_tensor[:, :channels_to_copy]
+
+ # Fill any remaining channels with appropriate noise
+ if noise_tensor.shape[1] < target_channels:
+ std = noise_tensor.std()
+ mean = noise_tensor.mean()
+ torch.manual_seed(seed + 999)
+ for c in range(noise_tensor.shape[1], target_channels):
+ corrected_tensor[:, c] = torch.randn((batch_size, height, width),
+ device=noise_tensor.device,
+ dtype=noise_tensor.dtype) * std + mean
+
+ noise_tensor = corrected_tensor
+ print(f"โ Adjusted to {target_channels} channels")
+
+ return noise_tensor
+
+def add_curl_noise_to_tensor(tensor_class):
+ """
+ Add curl noise implementation to ShaderToTensor class
+ Call this function to monkey patch the ShaderToTensor class
+
+ Args:
+ tensor_class: The ShaderToTensor class to modify
+ """
+ def curl_noise(cls, p, time, scale, warp_strength, phase_shift, octaves=3.0, seed=0, shape_type="none", shape_mask_strength=1.0, color_scheme="none", color_intensity=0.8):
+ """
+ Curl noise implementation for ShaderToTensor
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ time: Animation time
+ scale: Scale factor
+ warp_strength: Warp intensity
+ phase_shift: Controls advection time step
+ octaves: Number of detail levels (1-5) and pattern type
+ seed: Random seed
+ shape_type: Type of shape mask to apply ("none" for no mask)
+ shape_mask_strength: Strength of shape mask application
+ color_scheme: Color scheme to apply (e.g., "rainbow", "plasma", etc.)
+ color_intensity: Intensity of color effect (0.0 to 1.0)
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ # Debug print to confirm this method is being called with the right params
+ print(f">>> curl_noise method called with scale={scale}, warp={warp_strength}, phase={phase_shift}, octaves={octaves}")
+ print(f">>> Shape params: type={shape_type}, strength={shape_mask_strength}")
+ print(f">>> Color params: scheme={color_scheme}, intensity={color_intensity}")
+
+ # Create parameter dictionary from individual params
+ shader_params = {
+ "shaderScale": scale,
+ "shaderWarpStrength": warp_strength,
+ "shaderPhaseShift": phase_shift,
+ "shaderOctaves": octaves,
+ "time": time,
+ "shaderShapeType": shape_type,
+ "shaderShapeStrength": shape_mask_strength,
+ "colorScheme": color_scheme,
+ "shaderColorIntensity": color_intensity
+ }
+
+ # Get shape from p
+ batch, height, width, _ = p.shape
+
+ # Use the CurlNoiseGenerator directly
+ curl_noise = CurlNoiseGenerator.get_curl_noise(
+ batch_size=batch,
+ height=height,
+ width=width,
+ shader_params=shader_params,
+ device=p.device,
+ seed=seed # Pass the seed to the generator
+ )
+
+ # Convert from [B, 4, H, W] to [B, H, W, 1] for consistency with other functions
+ return curl_noise.permute(0, 2, 3, 1)[:, :, :, 0:1]
+
+ # Add the method to the class
+ setattr(tensor_class, 'curl_noise', classmethod(curl_noise))
+
+ def curl_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ """
+ Method that directly takes the full shader_params dictionary
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Full parameter dictionary from JSON
+ time: Animation time
+ device: Device to create tensor on
+ seed: Random seed
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ print(f">>> curl_noise_with_params called with full params dictionary")
+ print(f">>> Raw params: {shader_params}")
+
+ # Add time to params if not present
+ if "time" not in shader_params:
+ shader_params["time"] = time
+
+ # Add base_seed for temporal coherence if not present
+ if "base_seed" not in shader_params:
+ shader_params["base_seed"] = seed
+
+ # Extract target_channels if present
+ target_channels = shader_params.get("target_channels", 4)
+
+ # Create coordinate grid for curl noise
+ y, x = torch.meshgrid(
+ torch.linspace(0, 1, height, device=device),
+ torch.linspace(0, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ coords = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Set deterministic seed to ensure consistent results
+ torch.manual_seed(seed)
+
+ # Generate noise
+ noise = CurlNoiseGenerator.get_curl_noise(
+ batch_size=batch_size,
+ height=height,
+ width=width,
+ shader_params=shader_params,
+ device=device,
+ seed=seed,
+ target_channels=target_channels
+ )
+
+ # Convert to BHWC format for compatibility with expected outputs
+ # Just take the first channel for compatibility with existing methods
+ result = noise.permute(0, 2, 3, 1)[:, :, :, 0:1]
+ print(f">>> curl_noise_with_params produced tensor shape: {result.shape}")
+
+ return result
+
+ # Add the new method to the class
+ setattr(tensor_class, 'curl_noise_with_params', classmethod(curl_noise_with_params))
+
+ print("=== Added curl_noise functionality to ShaderToTensor ===") # Added print statement
+
+ # Update shader_noise_to_tensor to handle curl_noise type
+ original_shader_noise_to_tensor = tensor_class.shader_noise_to_tensor
+
+ @classmethod
+ def new_shader_noise_to_tensor(cls, batch_size=1, height=64, width=64, shader_type="tensor_field",
+ visualization_type=3, scale=1.0, phase_shift=0.0,
+ warp_strength=0.0, time=0.0, device="cuda", seed=0, octaves=3.0,
+ shader_params=None, shape_type="none", shape_mask_strength=1.0,
+ color_scheme="none", color_intensity=0.8):
+ """Updated shader_noise_to_tensor with curl_noise support"""
+ print(f"Generating {shader_type} noise with scale={scale}, octaves={octaves}, warp={warp_strength}, phase={phase_shift}")
+ print(f"Shape params: type={shape_type}, strength={shape_mask_strength}")
+ print(f"Color params: scheme={color_scheme}, intensity={color_intensity}")
+
+ # Check if we have a shader_params dictionary passed
+ has_shader_params = shader_params is not None
+ if has_shader_params:
+ print(f"Full shader_params provided: {shader_params}")
+
+ # Handle curl_noise shader type explicitly
+ if shader_type == "curl_noise" or shader_type == "curl":
+ print("EXECUTING CURL_NOISE BRANCH")
+
+ # If we have the full params dict and the new method, use it
+ if has_shader_params and hasattr(cls, 'curl_noise_with_params'):
+ # Ensure shape parameters are in the shader_params
+ if "shaderShapeType" not in shader_params and "shape_type" not in shader_params:
+ shader_params["shaderShapeType"] = shape_type
+ if "shaderShapeStrength" not in shader_params and "shapemaskstrength" not in shader_params:
+ shader_params["shaderShapeStrength"] = shape_mask_strength
+ # Ensure color parameters are in the shader_params
+ if "colorScheme" not in shader_params:
+ shader_params["colorScheme"] = color_scheme
+ if "shaderColorIntensity" not in shader_params and "intensity" not in shader_params:
+ shader_params["shaderColorIntensity"] = color_intensity
+
+ return cls.curl_noise_with_params(
+ batch_size, height, width, shader_params, time, device, seed
+ )
+
+ # Otherwise use the original implementation
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Set deterministic seed to ensure consistent results with same seed
+ torch.manual_seed(seed)
+
+ # Generate curl_noise
+ result = cls.curl_noise(
+ p, time, scale, warp_strength, phase_shift, octaves, seed,
+ shape_type, shape_mask_strength, color_scheme, color_intensity
+ )
+
+ print(f"Curl noise raw result shape: {result.shape}")
+
+ # Ensure result has the right shape [batch, height, width, 1]
+ if len(result.shape) == 3: # Missing dimension
+ result = result.unsqueeze(-1)
+
+ # Result should now be in [B, H, W, C] format
+ print(f"Final curl noise shape (BHWC format): {result.shape}")
+
+ return result
+ else:
+ # Call original method for other shader types
+ return original_shader_noise_to_tensor(
+ batch_size, height, width, shader_type, visualization_type,
+ scale, phase_shift, warp_strength, time, device, seed, octaves,
+ shader_params, shape_type, shape_mask_strength
+ )
+
+ # Replace the original method only if it hasn't been replaced by other noise integrations already
+ if hasattr(tensor_class, '_original_shader_noise_to_tensor'):
+ # If already replaced, we need to update the existing method to handle curl_noise
+ existing_method = tensor_class.shader_noise_to_tensor
+
+ @classmethod
+ def updated_shader_noise_to_tensor(cls, batch_size=1, height=64, width=64, shader_type="tensor_field",
+ visualization_type=3, scale=1.0, phase_shift=0.0,
+ warp_strength=0.0, time=0.0, device="cuda", seed=0, octaves=3.0,
+ shader_params=None, shape_type="none", shape_mask_strength=1.0,
+ color_scheme="none", color_intensity=0.8):
+ # Handle curl_noise shader type explicitly
+ if shader_type == "curl_noise" or shader_type == "curl":
+ print("EXECUTING CURL_NOISE BRANCH")
+
+ # If we have the full params dict and the new method, use it
+ has_shader_params = shader_params is not None
+ if has_shader_params:
+ # Ensure shape parameters are in the shader_params
+ if "shaderShapeType" not in shader_params and "shape_type" not in shader_params:
+ shader_params["shaderShapeType"] = shape_type
+ if "shaderShapeStrength" not in shader_params and "shapemaskstrength" not in shader_params:
+ shader_params["shaderShapeStrength"] = shape_mask_strength
+ # Ensure color parameters are in the shader_params
+ if "colorScheme" not in shader_params:
+ shader_params["colorScheme"] = color_scheme
+ if "shaderColorIntensity" not in shader_params and "intensity" not in shader_params:
+ shader_params["shaderColorIntensity"] = color_intensity
+
+ return cls.curl_noise_with_params(
+ batch_size, height, width, shader_params, time, device, seed
+ )
+
+ # Otherwise use the original implementation
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Set deterministic seed
+ torch.manual_seed(seed)
+
+ # Generate curl_noise
+ result = cls.curl_noise(
+ p, time, scale, warp_strength, phase_shift, octaves, seed,
+ shape_type, shape_mask_strength, color_scheme, color_intensity
+ )
+
+ # Ensure result has the right shape
+ if len(result.shape) == 3:
+ result = result.unsqueeze(-1)
+
+ print(f"Final curl noise shape: {result.shape}")
+
+ return result
+ else:
+ # Call existing method for other shader types
+ return existing_method(
+ batch_size, height, width, shader_type, visualization_type,
+ scale, phase_shift, warp_strength, time, device, seed, octaves,
+ shader_params, shape_type, shape_mask_strength
+ )
+
+ setattr(tensor_class, 'shader_noise_to_tensor', updated_shader_noise_to_tensor)
+ else:
+ # Save original method for later use
+ setattr(tensor_class, '_original_shader_noise_to_tensor', original_shader_noise_to_tensor)
+ setattr(tensor_class, 'shader_noise_to_tensor', new_shader_noise_to_tensor)
+
+def integrate_curl_noise():
+ """
+ Integrate curl noise implementation into existing code
+ This should be called in __init__.py to ensure curl noise is available
+ """
+ # Import the necessary classes
+ from ..shader_to_tensor import ShaderToTensor
+
+ # Add the curl noise implementation to ShaderToTensor
+ add_curl_noise_to_tensor(ShaderToTensor)
+
+ print("Curl Noise implementation integrated successfully")
+
+def add_temporal_coherent_curl_to_shader_params(shader_params, animation_frame, base_seed, fps=24, duration=1.0):
+ """
+ Adds temporal coherence parameters to the shader_params for use with video frames
+
+ Args:
+ shader_params: Original shader parameters dictionary
+ animation_frame: Current frame number (0-indexed)
+ base_seed: Base seed to use for the entire animation
+ fps: Frames per second (for time calculation)
+ duration: Animation duration in seconds
+
+ Returns:
+ Updated shader_params with frame-specific temporal values
+ """
+ # Get the phase shift parameter (either with shaderPhaseShift or phase_shift key)
+ phase_shift = shader_params.get("shaderPhaseShift", shader_params.get("phase_shift", 0.5))
+
+ # Calculate animation length from fps and duration
+ animation_length = int(fps * duration)
+
+ # Get useTemporalCoherence parameter (default to True for better results)
+ use_temporal_coherence = shader_params.get("useTemporalCoherence", True)
+
+ # Get color scheme parameters and preserve them for video
+ color_scheme = shader_params.get("colorScheme", "none")
+ color_intensity = shader_params.get("shaderColorIntensity", shader_params.get("intensity", 0.8))
+
+ # Get temporal parameters from helper function
+ temporal_params = CurlNoiseGenerator.add_temporal_coherent_curl_to_shader_params(
+ animation_frame, animation_length, phase_shift, base_seed, use_temporal_coherence,
+ color_scheme, color_intensity
+ )
+
+ # Create a copy of shader_params to avoid modifying the original
+ new_params = shader_params.copy()
+
+ # Update with temporal parameters
+ new_params["time"] = temporal_params["time"]
+ new_params["base_seed"] = base_seed
+ new_params["temporal_coherence"] = use_temporal_coherence
+ new_params["seed"] = temporal_params["seed"]
+
+ # Update the phase shift parameter for current frame
+ new_params["shaderPhaseShift"] = temporal_params["phase"]
+
+ # Ensure color parameters are preserved
+ new_params["colorScheme"] = color_scheme
+ new_params["shaderColorIntensity"] = color_intensity
+
+ # Set these explicitly for cleaner integration
+ print(f"Video frame {animation_frame}/{animation_length}: time={temporal_params['time']:.3f}, seed={temporal_params['seed']}, phase={temporal_params['phase']:.3f}")
+ print(f"Color scheme: {color_scheme}, intensity: {color_intensity}")
+
+ return new_params
\ No newline at end of file
diff --git a/shaders/domain_warp.py b/shaders/domain_warp.py
new file mode 100644
index 0000000..d0ba02d
--- /dev/null
+++ b/shaders/domain_warp.py
@@ -0,0 +1,2120 @@
+import torch
+import math
+# Import ShaderParamsReader for apply_shape_mask functionality
+from ..shader_params_reader import ShaderParamsReader
+
+class DomainWarpGenerator:
+ """
+ PyTorch implementation of domain warp animation that closely matches
+ the WebGL domain warp shader from shader_renderer.js
+
+ This class generates domain warping noise, where one noise function distorts
+ the input coordinates for another noise function, creating complex, swirling
+ patterns used to influence the sampling process in image generation.
+
+ Now enhanced with true temporal coherence for smooth animations.
+ """
+
+ @staticmethod
+ def get_domain_warp(batch_size, height, width, shader_params, device="cuda", seed=0):
+ """
+ Generate domain warp noise tensor that matches the WebGL implementation
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Dictionary containing parameters from shader_params.json
+ device: Device to create tensor on
+ seed: Random seed value to ensure different patterns with different seeds
+
+ Returns:
+ Tensor with shape [batch_size, channels, height, width]
+ """
+ # Get debugger instance
+ # debugger = get_debugger()
+
+ # Extract parameters from shader_params - check both original and converted names
+ # This ensures compatibility with both raw JSON params and converted params
+ scale = shader_params.get("shaderScale", shader_params.get("scale", 1.0))
+ warp_strength = shader_params.get("shaderWarpStrength", shader_params.get("warp_strength", 0.5))
+ phase_shift = shader_params.get("shaderPhaseShift", shader_params.get("phase_shift", 0.5))
+ octaves = shader_params.get("shaderOctaves", shader_params.get("octaves", 1))
+ time = shader_params.get("time", 0.0) # This would ideally come from the animation state
+
+ # Extract temporal coherence parameters - CHANGED: Default to False
+ base_seed = shader_params.get("base_seed", seed) # Use provided seed as default base_seed
+ use_temporal_coherence = shader_params.get("temporal_coherence", False) # CHANGED: Default to False
+
+ # Extract target channel count if provided
+ target_channels = shader_params.get("target_channels", 9) # Default to 9 if not specified
+
+ # --- Model Specific Channel Overrides ---
+ # Check if the model is CosmosVideo and override the channel count if needed
+ model_class = shader_params.get("model_class", "")
+ inner_model_class = shader_params.get("inner_model_class", "")
+
+ if inner_model_class == "CosmosVideo":
+ target_channels = 16
+ # Update shader_params as well for consistency downstream
+ shader_params["target_channels"] = 16
+ elif model_class == "CosmosVideo": # Fallback check on outer model class
+ target_channels = 16
+ shader_params["target_channels"] = 16
+ elif inner_model_class == "ACEStep":
+ target_channels = 8
+ shader_params["target_channels"] = 8
+
+ # Apply seed variation if provided
+ seed_variation = shader_params.get("seed_variation", 0)
+ if seed_variation > 0:
+ seed = seed + seed_variation
+ # if debugger.enabled and debugger.debug_level >= 2:
+ # print(f"Using seed variation {seed_variation}, adjusted seed to {seed}")
+
+ # FIXED: If not using temporal coherence, use the current seed value not base_seed
+ # This ensures different noise patterns with different seeds
+ current_seed = base_seed if use_temporal_coherence else seed
+
+ # Extract shape mask parameters
+ shape_type = shader_params.get("shaderShapeType", shader_params.get("shape_type", "none"))
+ shape_mask_strength = shader_params.get("shaderShapeStrength", shader_params.get("shapemaskstrength", 1.0))
+
+ # Extract color scheme parameters
+ color_scheme = shader_params.get("colorScheme", "none")
+ color_intensity = shader_params.get("shaderColorIntensity", shader_params.get("intensity", 0.8))
+
+ # Print params being used for debugging
+ # print(f"DomainWarp: Using shader_type: {shader_params.get('shader_type', 'domain_warp')}")
+ # print(f"DomainWarp: Scale={scale}, Warp Strength={warp_strength}, Phase Shift={phase_shift}, Octaves={octaves}")
+ # print(f"DomainWarp: Temporal settings: time={time}, base_seed={base_seed}, coherence={use_temporal_coherence}")
+ # print(f"DomainWarp: Shape Mask: type={shape_type}, strength={shape_mask_strength}")
+ # print(f"DomainWarp: Color Scheme: scheme={color_scheme}, intensity={color_intensity}")
+ # print(f"DomainWarp: Target Channels: {target_channels}")
+
+ # Log shader parameters for debugging
+ # if debugger.enabled and debugger.debug_level >= 1:
+ # debugger.log_generation_operation(
+ # "domain_warp_start",
+ # {"shader_params": shader_params},
+ # None,
+ # {
+ # "scale": scale,
+ # "warp_strength": warp_strength,
+ # "phase_shift": phase_shift,
+ # "octaves": octaves,
+ # "time": time,
+ # "seed": seed,
+ # "target_channels": target_channels
+ # }
+ # )
+
+ # Create coordinate grid (normalized to [0, 1] for domain warp shader)
+ y, x = torch.meshgrid(
+ torch.linspace(0, 1, height, device=device),
+ torch.linspace(0, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Track the coordinate tensor
+ # if debugger.enabled and debugger.debug_level >= 2:
+ # debugger.track_tensor_shape_history(p, "coordinates", "initial_creation")
+
+ # Generate domain warp noise based on octaves value
+ warp_type = int(octaves % 4) # Same as in shader: int(mod(u_octaves, 4.0))
+
+ # Helper for lerp
+ def lerp(a, b, t):
+ return a + (b - a) * t
+
+ # Helper for HSV to RGB conversion (from curl_noise.py)
+ def hsv_to_rgb(h, s, v, device):
+ # h, s, v expected shapes [B, 1, H, W]
+ # Ensure all inputs are on the correct device
+ h = h.to(device)
+ s = s.to(device)
+ v = v.to(device)
+
+ c = v * s
+ h_prime = h * 6.0
+ x = c * (1.0 - torch.abs(torch.fmod(h_prime, 2.0) - 1.0))
+ m = v - c
+
+ r, g, b = torch.zeros_like(h), torch.zeros_like(h), torch.zeros_like(h)
+
+ mask0 = (h_prime < 1.0)
+ r[mask0], g[mask0], b[mask0] = c[mask0], x[mask0], 0.0
+ mask1 = (h_prime >= 1.0) & (h_prime < 2.0)
+ r[mask1], g[mask1], b[mask1] = x[mask1], c[mask1], 0.0
+ mask2 = (h_prime >= 2.0) & (h_prime < 3.0)
+ r[mask2], g[mask2], b[mask2] = 0.0, c[mask2], x[mask2]
+ mask3 = (h_prime >= 3.0) & (h_prime < 4.0)
+ r[mask3], g[mask3], b[mask3] = 0.0, x[mask3], c[mask3]
+ mask4 = (h_prime >= 4.0) & (h_prime < 5.0)
+ r[mask4], g[mask4], b[mask4] = x[mask4], 0.0, c[mask4]
+ mask5 = (h_prime >= 5.0)
+ r[mask5], g[mask5], b[mask5] = c[mask5], 0.0, x[mask5]
+
+ r, g, b = r + m, g + m, b + m
+ return r, g, b
+
+ # Helper for color stops interpolation
+ def interpolate_colors(stops, t, device):
+ # stops: list of [value, color_tensor]
+ # t: normalized value tensor [B, 1, H, W]
+
+ # Find the two stops t falls between
+ idx = torch.zeros_like(t, dtype=torch.long)
+ for i in range(len(stops) - 1):
+ idx = torch.where((t >= stops[i][0]) & (t < stops[i+1][0]), torch.full_like(idx, i), idx)
+ idx = torch.where(t >= stops[-1][0], torch.full_like(idx, len(stops) - 2), idx) # Handle >= last stop value
+
+ # Gather the start and end stops based on idx
+ # Initialize with target spatial shape, ensure first stop color tensor is on the correct device
+ final_color_shape = (t.shape[0], 3, t.shape[2], t.shape[3]) # B, C, H, W
+ final_color = torch.zeros(final_color_shape, device=device)
+
+ for i in range(len(stops) - 1):
+ mask = (idx == i) # Shape [B, 1, H, W]
+ t0_val, c0_list = stops[i]
+ t1_val, c1_list = stops[i+1]
+
+ c0 = torch.tensor(c0_list, device=device).view(1, 3, 1, 1) # Shape [1, 3, 1, 1]
+ c1 = torch.tensor(c1_list, device=device).view(1, 3, 1, 1) # Shape [1, 3, 1, 1]
+
+ # Normalize t within the segment [t0, t1] -> [0, 1] for all pixels
+ denominator = (t1_val - t0_val + 1e-8) # Avoid division by zero
+ local_t_all = (t - t0_val) / denominator
+ local_t_clamped = torch.clamp(local_t_all, 0.0, 1.0) # Shape [B, 1, H, W]
+
+ # Lerp the colors for this segment - c0/c1 will broadcast
+ interp_color = lerp(c0, c1, local_t_clamped) # Shape [B, 3, H, W]
+
+ # Apply the interpolated color where the mask is true
+ final_color = torch.where(mask.expand_as(interp_color), interp_color, final_color)
+
+ return final_color[:, 0:1], final_color[:, 1:2], final_color[:, 2:3] # R, G, B
+
+ # Generate warped noise - pass phase_shift to the domain_warp function
+ try:
+ # with debugger.time_operation("domain_warp_generation") if debugger.enabled else contextlib.nullcontext():
+ result = DomainWarpGenerator._domain_warp_with_phase(
+ p, device, octaves, seed, 0, warp_type, scale, warp_strength, phase_shift, time
+ )
+
+ # Track the initial result tensor
+ # if debugger.enabled and debugger.debug_level >= 2:
+ # debugger.track_tensor_shape_history(result, "domain_warp_result", "after_generation")
+ # debugger.analyze_tensor(result, "domain_warp_raw")
+ except Exception as e:
+ # if debugger.enabled:
+ # debugger.add_warning(f"Error generating domain warp: {str(e)}", category="generation_error")
+ # print(f"โ Error generating domain warp: {str(e)}")
+ # Fallback to random noise if generation fails
+ result = torch.randn((batch_size, height, width, 1), device=device)
+
+ # Apply contrast adjustment in addition to the internal phase shift effects
+ contrast = 1.0 + phase_shift * 0.5 # Reduced effect since we're now using phase in the patterns
+ result *= contrast
+
+ # Apply shape mask if requested
+ if shape_type not in ["none", "0"] and shape_mask_strength > 0:
+ try:
+ # Create shape mask using ShaderParamsReader
+ # Transform p from [0,1] to [-1,1] for shape mask --- This is no longer needed for the updated reader
+ # p_normalized = p * 2.0 - 1.0
+
+ # Use same base_seed for shape mask to ensure temporal coherence
+ # The seed for shape_mask itself is handled internally by apply_shape_mask if needed,
+ # but we pass base_seed and use_temporal_coherence for its own logic.
+ current_mask_seed = base_seed if use_temporal_coherence else seed
+ # torch.manual_seed(current_mask_seed + 500) # Seeding is now internal to reader if necessary for its own ops
+
+ shape_mask = ShaderParamsReader.apply_shape_mask(
+ p, # p is already in [0,1] range
+ shape_type,
+ time=time,
+ base_seed=base_seed,
+ use_temporal_coherence=use_temporal_coherence
+ )
+
+ # Track shape mask tensor
+ # if debugger.enabled and debugger.debug_level >= 2:
+ # debugger.track_tensor_shape_history(shape_mask, "shape_mask", "after_creation")
+
+ # Apply shape mask to the noise
+ result = torch.lerp(result, result * shape_mask, shape_mask_strength)
+ except Exception as e:
+ # if debugger.enabled:
+ # debugger.add_warning(f"Error applying shape mask: {str(e)}", category="shape_mask_error")
+ pass
+ # Ensure output is in valid [-1, 1] range
+ result = torch.clamp(result, -1.0, 1.0)
+
+ # Convert from [batch, height, width, 1] to [batch, 1, height, width]
+ # Permute dimensions to match expected format
+ result = result.permute(0, 3, 1, 2) # [batch, 1, height, width]
+
+ # Track tensor after permutation
+ # if debugger.enabled and debugger.debug_level >= 2:
+ # debugger.track_tensor_shape_history(result, "result_permuted", "after_permutation")
+
+ # Create channel variations for color
+ if color_scheme != "none" and color_intensity > 0:
+ try:
+ # with debugger.time_operation("color_variations") if debugger.enabled else contextlib.nullcontext():
+ # Initialize base channel (red)
+ r_channel = result.clone()
+
+ # Scale intensity factor based on color_intensity
+ intensity_factor = 0.5 + color_intensity * 0.5 # Maps 0-1 to 0.5-1.0
+
+ # Apply color scheme specific variations
+ if color_scheme == "viridis":
+ # Viridis color scheme
+ # Ensure result (base noise for coloring) is normalized to [0,1] for interpolate_colors
+ # The 'result' tensor here is the single channel noise before colorization
+ normalized_value = (result - result.min()) / (result.max() - result.min() + 1e-8)
+
+ stops = [
+ (0.0, [0.267, 0.005, 0.329]), # #440154
+ (0.33, [0.188, 0.407, 0.553]), # #30678D
+ (0.66, [0.208, 0.718, 0.471]), # #35B778
+ (1.0, [0.992, 0.906, 0.143]) # #FDE724
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "plasma":
+ # Plasma color scheme from curl_noise.py
+ # 1. Generate proxy for x and y components (vx_proxy, vy_proxy)
+ # similar to how g_channel and b_channel are made for other vibrant schemes.
+ p_vx_proxy = p * (scale * 0.92) # Slightly different params for variety
+ p_vy_proxy = p * (scale * 1.08)
+
+ vx_proxy_bhwc = DomainWarpGenerator._domain_warp_with_phase(
+ p_vx_proxy, device, octaves, seed + 10, 0, warp_type, scale * 0.92,
+ warp_strength * (1.0 + 0.15 * intensity_factor),
+ phase_shift + 0.20 * intensity_factor, time
+ ) # [B, H, W, 1]
+ vy_proxy_bhwc = DomainWarpGenerator._domain_warp_with_phase(
+ p_vy_proxy, device, octaves, seed + 20, 0, warp_type, scale * 1.08,
+ warp_strength * (1.0 - 0.15 * intensity_factor),
+ phase_shift - 0.20 * intensity_factor, time
+ ) # [B, H, W, 1]
+
+ # No need for contrast application or permute yet for these proxies
+ # Ensure they are in [0,1] range if _domain_warp_with_phase output is not guaranteed
+ vx_proxy_bhwc = torch.clamp(vx_proxy_bhwc, 0.0, 1.0)
+ vy_proxy_bhwc = torch.clamp(vy_proxy_bhwc, 0.0, 1.0)
+
+ # Convert to [B, 1, H, W]
+ vx_proxy = vx_proxy_bhwc.permute(0, 3, 1, 2)
+ vy_proxy = vy_proxy_bhwc.permute(0, 3, 1, 2)
+
+ # 2. Calculate vangle
+ vangle = torch.atan2(vy_proxy, vx_proxy) # Output is [-pi, pi]
+ vangle = (vangle + math.pi) / (2 * math.pi) # Normalize to [0, 1]
+
+ # 3. Normalize base result for normalized_value
+ # 'result' is [B, 1, H, W] and in [-1, 1], normalize to [0, 1]
+ normalized_value = (result + 1.0) / 2.0
+
+ # 4. Prepare time_broadcast
+ time_tensor = torch.tensor(time, device=device, dtype=result.dtype)
+ time_broadcast = time_tensor.view(-1, 1, 1, 1) if time_tensor.numel() > 1 else time_tensor
+ if time_broadcast.dim() == 0: # Ensure it's at least 1D for view(-1,1,1,1)
+ time_broadcast = time_broadcast.unsqueeze(0)
+ time_broadcast = time_broadcast.view(-1, 1, 1, 1) # Ensure [B_or_1, 1, 1, 1]
+ if time_broadcast.shape[0] != r_channel.shape[0] and time_broadcast.shape[0] == 1:
+ time_broadcast = time_broadcast.expand(r_channel.shape[0], -1, -1, -1)
+
+ # 5. Apply plasma calculations
+ r_channel_plasma = 0.5 + 0.5 * torch.sin(vangle * 6.28318 + time_broadcast)
+ g_channel_plasma = 0.5 + 0.5 * torch.sin(vangle * 6.28318 + normalized_value * 3.14159 + time_broadcast * 2.0)
+ b_channel_plasma = 0.5 + 0.5 * torch.cos(vangle * 3.14159 + normalized_value * 6.28318 + time_broadcast * 3.0)
+
+ r_channel = r_channel_plasma
+ g_channel = g_channel_plasma
+ b_channel = b_channel_plasma
+
+ elif color_scheme == "inferno":
+ # Inferno color scheme from curl_noise.py
+ # Ensure result (base noise for coloring) is normalized to [0,1] for interpolate_colors
+ # The 'result' tensor here is the single channel noise before colorization [B, 1, H, W]
+ # It's already in [-1, 1] range from clamp, so normalize to [0, 1]
+ normalized_value = (result + 1.0) / 2.0
+
+ stops = [
+ (0.0, [0.001, 0.001, 0.016]),
+ (0.25, [0.259, 0.039, 0.408]),
+ (0.5, [0.576, 0.149, 0.404]),
+ (0.75, [0.867, 0.318, 0.227]),
+ (0.85, [0.988, 0.647, 0.039]),
+ (1.0, [0.988, 1.000, 0.643])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "magma":
+ # Magma color scheme from curl_noise.py
+ # Ensure result (base noise for coloring) is normalized to [0,1] for interpolate_colors
+ # The 'result' tensor here is the single channel noise before colorization [B, 1, H, W]
+ # It's already in [-1, 1] range from clamp, so normalize to [0, 1]
+ normalized_value = (result + 1.0) / 2.0
+
+ stops = [
+ (0.0, [0.001, 0.001, 0.016]),
+ (0.25, [0.231, 0.059, 0.439]),
+ (0.5, [0.549, 0.161, 0.506]),
+ (0.75, [0.871, 0.288, 0.408]),
+ (0.85, [0.996, 0.624, 0.427]),
+ (1.0, [0.988, 0.992, 0.749])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "turbo":
+ # Turbo color scheme from curl_noise.py
+ # Ensure result (base noise for coloring) is normalized to [0,1] for interpolate_colors
+ # The 'result' tensor here is the single channel noise before colorization [B, 1, H, W]
+ # It's already in [-1, 1] range from clamp, so normalize to [0, 1]
+ normalized_value = (result + 1.0) / 2.0
+
+ stops = [
+ (0.0, [0.188, 0.071, 0.235]), # #30123b
+ (0.25, [0.275, 0.408, 0.859]), # #4669db
+ (0.5, [0.149, 0.749, 0.549]), # #26bf8c
+ (0.65, [0.831, 1.000, 0.314]), # #d4ff50
+ (0.85, [0.980, 0.718, 0.298]), # #fab74c
+ (1.0, [0.729, 0.004, 0.000]) # #ba0100
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "jet":
+ # Jet color scheme from curl_noise.py
+ # Ensure result (base noise for coloring) is normalized to [0,1] for interpolate_colors
+ # The 'result' tensor here is the single channel noise before colorization [B, 1, H, W]
+ # It's already in [-1, 1] range from clamp, so normalize to [0, 1]
+ normalized_value = (result + 1.0) / 2.0
+
+ stops = [
+ (0.0, [0.000, 0.000, 0.498]), # #00007f
+ (0.125, [0.000, 0.000, 1.000]), # #0000ff blue
+ (0.375, [0.000, 1.000, 1.000]), # #00ffff cyan
+ (0.625, [1.000, 1.000, 0.000]), # #ffff00 yellow
+ (0.875, [1.000, 0.000, 0.000]), # #ff0000 red
+ (1.0, [0.498, 0.000, 0.000]) # #7f0000 dark red
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "rainbow":
+ # Rainbow color scheme using HSV conversion, similar to curl_noise.py
+ # The 'result' tensor is [B, 1, H, W] and in [-1, 1]
+
+ # Normalize base result for HUE component [0, 1]
+ hue_component = (result + 1.0) / 2.0
+
+ # Create a phase-shifted version for VALUE component [0, 1]
+ # We can use a rolled version of the result or a slightly modified noise
+ # For simplicity, let's use a rolled version for now.
+ # Roll factor can be a fraction of width or height.
+ roll_factor_h = int(height * 0.1 * intensity_factor)
+ roll_factor_w = int(width * 0.1 * intensity_factor)
+ rolled_result = torch.roll(result, shifts=(roll_factor_h, roll_factor_w), dims=(2, 3))
+ value_component = (rolled_result + 1.0) / 2.0
+
+ # Fixed saturation
+ saturation_component = torch.ones_like(hue_component) * 0.8
+
+ # Convert HSV to RGB
+ # hsv_to_rgb expects [B, 1, H, W] inputs
+ r_channel, g_channel, b_channel = hsv_to_rgb(hue_component, saturation_component, value_component, device)
+
+ elif color_scheme == "magma": # Keep magma separate from rainbow now
+ # Magma color scheme from curl_noise.py
+ # Ensure result (base noise for coloring) is normalized to [0,1] for interpolate_colors
+ # The 'result' tensor here is the single channel noise before colorization [B, 1, H, W]
+ # It's already in [-1, 1] range from clamp, so normalize to [0, 1]
+ normalized_value = (result + 1.0) / 2.0
+
+ stops = [
+ (0.0, [0.001, 0.001, 0.016]),
+ (0.25, [0.231, 0.059, 0.439]),
+ (0.5, [0.549, 0.161, 0.506]),
+ (0.75, [0.871, 0.288, 0.408]),
+ (0.85, [0.996, 0.624, 0.427]),
+ (1.0, [0.988, 0.992, 0.749])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "cool":
+ # Cool color scheme from curl_noise.py
+ # Interpolates between Cyan and Magenta
+ # 'result' is [B, 1, H, W] and in [-1, 1]
+ normalized_value = (result + 1.0) / 2.0 # Normalize to [0, 1]
+
+ c0 = torch.tensor([0.0, 1.0, 1.0], device=device).view(1, 3, 1, 1) # Cyan
+ c1 = torch.tensor([1.0, 0.0, 1.0], device=device).view(1, 3, 1, 1) # Magenta
+
+ color_lerped = lerp(c0, c1, normalized_value)
+ r_channel, g_channel, b_channel = color_lerped[:, 0:1], color_lerped[:, 1:2], color_lerped[:, 2:3]
+
+ elif color_scheme == "hot":
+ # Hot color scheme from curl_noise.py
+ normalized_value = (result + 1.0) / 2.0
+ stops = [
+ (0.0, [0.0, 0.0, 0.0]), # Black
+ (0.375, [1.0, 0.0, 0.0]), # Red
+ (0.75, [1.0, 1.0, 0.0]), # Yellow
+ (1.0, [1.0, 1.0, 1.0]) # White
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "parula":
+ normalized_value = (result + 1.0) / 2.0
+ stops = [
+ (0.0, [0.208, 0.165, 0.529]),
+ (0.25, [0.059, 0.361, 0.867]),
+ (0.5, [0.000, 0.710, 0.651]),
+ (0.75, [1.000, 0.765, 0.216]),
+ (1.0, [0.988, 0.996, 0.643])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "pink":
+ normalized_value = (result + 1.0) / 2.0
+ stops = [
+ (0.0, [0.05, 0.05, 0.05]),
+ (0.5, [1.0, 0.0, 1.0]),
+ (1.0, [1.0, 1.0, 1.0])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "bone":
+ normalized_value = (result + 1.0) / 2.0
+ stops = [
+ (0.0, [0.0, 0.0, 0.0]),
+ (0.375, [0.329, 0.329, 0.455]),
+ (0.75, [0.627, 0.757, 0.757]),
+ (1.0, [1.0, 1.0, 1.0])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "ocean":
+ normalized_value = (result + 1.0) / 2.0
+ stops = [
+ (0.0, [0.0, 0.0, 0.0]),
+ (0.33, [0.0, 0.0, 0.6]),
+ (0.66, [0.0, 0.6, 1.0]),
+ (1.0, [0.6, 1.0, 1.0])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "terrain":
+ normalized_value = (result + 1.0) / 2.0
+ stops = [
+ (0.0, [0.2, 0.2, 0.6]),
+ (0.33, [0.0, 0.8, 0.4]),
+ (0.66, [1.0, 0.8, 0.0]),
+ (1.0, [1.0, 1.0, 1.0])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "neon":
+ normalized_value = (result + 1.0) / 2.0
+ stops = [
+ (0.0, [1.0, 0.0, 1.0]),
+ (0.5, [0.0, 1.0, 1.0]),
+ (1.0, [1.0, 1.0, 0.0])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "fire":
+ normalized_value = (result + 1.0) / 2.0
+ stops = [
+ (0.0, [0.0, 0.0, 0.0]),
+ (0.25, [1.0, 0.0, 0.0]),
+ (0.6, [1.0, 1.0, 0.0]),
+ (1.0, [1.0, 1.0, 1.0])
+ ]
+ r_channel, g_channel, b_channel = interpolate_colors(stops, normalized_value, device)
+
+ elif color_scheme == "blue_red":
+ normalized_value = (result + 1.0) / 2.0
+ c0 = torch.tensor([0.0, 0.0, 1.0], device=device).view(1, 3, 1, 1) # Blue
+ c1 = torch.tensor([1.0, 0.0, 0.0], device=device).view(1, 3, 1, 1) # Red
+ color_lerped = lerp(c0, c1, normalized_value)
+ r_channel, g_channel, b_channel = color_lerped[:, 0:1], color_lerped[:, 1:2], color_lerped[:, 2:3]
+
+ elif color_scheme == "autumn":
+ normalized_value = (result + 1.0) / 2.0
+ c0 = torch.tensor([1.0, 0.0, 0.0], device=device).view(1, 3, 1, 1) # Red
+ c1 = torch.tensor([1.0, 1.0, 0.0], device=device).view(1, 3, 1, 1) # Yellow
+ color_lerped = lerp(c0, c1, normalized_value)
+ r_channel, g_channel, b_channel = color_lerped[:, 0:1], color_lerped[:, 1:2], color_lerped[:, 2:3]
+
+ elif color_scheme == "winter":
+ normalized_value = (result + 1.0) / 2.0
+ c0 = torch.tensor([0.0, 0.0, 1.0], device=device).view(1, 3, 1, 1) # Blue
+ c1 = torch.tensor([0.0, 1.0, 0.5], device=device).view(1, 3, 1, 1) # Greenish-Cyan
+ color_lerped = lerp(c0, c1, normalized_value)
+ r_channel, g_channel, b_channel = color_lerped[:, 0:1], color_lerped[:, 1:2], color_lerped[:, 2:3]
+
+ elif color_scheme == "spring":
+ normalized_value = (result + 1.0) / 2.0
+ c0 = torch.tensor([1.0, 0.0, 1.0], device=device).view(1, 3, 1, 1) # Magenta
+ c1 = torch.tensor([1.0, 1.0, 0.0], device=device).view(1, 3, 1, 1) # Yellow
+ color_lerped = lerp(c0, c1, normalized_value)
+ r_channel, g_channel, b_channel = color_lerped[:, 0:1], color_lerped[:, 1:2], color_lerped[:, 2:3]
+
+ elif color_scheme == "summer":
+ normalized_value = (result + 1.0) / 2.0
+ c0 = torch.tensor([0.0, 0.5, 0.4], device=device).view(1, 3, 1, 1) # Dark Green
+ c1 = torch.tensor([1.0, 1.0, 0.4], device=device).view(1, 3, 1, 1) # Yellow
+ color_lerped = lerp(c0, c1, normalized_value)
+ r_channel, g_channel, b_channel = color_lerped[:, 0:1], color_lerped[:, 1:2], color_lerped[:, 2:3]
+
+ elif color_scheme == "copper":
+ normalized_value = (result + 1.0) / 2.0
+ c0 = torch.tensor([0.0, 0.0, 0.0], device=device).view(1, 3, 1, 1) # Black
+ c1 = torch.tensor([1.0, 0.6, 0.4], device=device).view(1, 3, 1, 1) # Copper
+ color_lerped = lerp(c0, c1, normalized_value)
+ r_channel, g_channel, b_channel = color_lerped[:, 0:1], color_lerped[:, 1:2], color_lerped[:, 2:3]
+
+ elif color_scheme in ["complementary"]:
+ # For contrasting schemes, create more opposing patterns
+ # Use negated and inverted variations of the original pattern
+
+ # Create offset and modified coordinates
+ p_g = torch.roll(p, shifts=int(width * 0.1 * intensity_factor), dims=2)
+ p_b = torch.roll(p, shifts=int(height * 0.1 * intensity_factor), dims=1)
+
+ # Generate warped patterns with contrasting parameters
+ g_warp_result = DomainWarpGenerator._domain_warp_with_phase(
+ p_g, device, octaves, seed, (warp_type + 1) % 4,
+ scale * 1.1,
+ warp_strength * (1.0 - 0.3 * intensity_factor),
+ phase_shift + 0.5, time
+ )
+
+ b_warp_result = DomainWarpGenerator._domain_warp_with_phase(
+ p_b, device, octaves, seed, (warp_type + 2) % 4,
+ scale * 0.9,
+ warp_strength * (1.0 + 0.3 * intensity_factor),
+ phase_shift + 0.5, time
+ )
+
+ # Apply contrast and permute
+ g_warp_result *= contrast * 0.9 # Slightly reduced contrast
+ b_warp_result *= contrast * 1.1 # Slightly increased contrast
+ g_warp_result = torch.clamp(g_warp_result, -1.0, 1.0).permute(0, 3, 1, 2)
+ b_warp_result = torch.clamp(b_warp_result, -1.0, 1.0).permute(0, 3, 1, 2)
+
+ # Use these as green and blue channels
+ g_channel = g_warp_result
+ b_channel = b_warp_result
+
+ else:
+ # For other schemes, use more subtle variations with phase shifts
+ # Original coordinates with slight offsets
+
+ # Create subtly modified versions of the pattern
+ g_channel = torch.roll(r_channel, shifts=int(height * 0.05 * intensity_factor), dims=2)
+ b_channel = torch.roll(r_channel, shifts=int(width * 0.05 * intensity_factor), dims=3)
+
+ # Apply subtle transformations
+ if intensity_factor > 0.6:
+ # For higher intensity, add more pronounced color variations
+ g_channel = g_channel * (1.0 - 0.2 * intensity_factor) + 0.1 * intensity_factor
+ b_channel = b_channel * (1.0 - 0.3 * intensity_factor) - 0.1 * intensity_factor
+
+ except Exception as e:
+ # if debugger.enabled:
+ # debugger.add_warning(f"Error creating color variations: {str(e)}", category="color_error")
+ # print(f"โ ๏ธ Error creating color variations: {str(e)}")
+ # Fallback to simple replication
+ r_channel = result
+ g_channel = result
+ b_channel = result
+ else:
+ # Without color scheme, duplicate the channel
+ r_channel = result
+ g_channel = result
+ b_channel = result
+
+ # Alpha channel remains the original noise
+ a_channel = result
+
+ # Combine all channels
+ result = torch.cat([r_channel, g_channel, b_channel, a_channel], dim=1)
+
+ # Track results after RGB channel combination
+ # if debugger.enabled and debugger.debug_level >= 2:
+ # debugger.track_tensor_shape_history(result, "result_rgba", "after_color_channels")
+ # debugger.analyze_tensor(result, "domain_warp_rgba")
+
+ # Expand to target_channels if specified
+ try:
+ # with debugger.time_operation("channel_expansion") if debugger.enabled else contextlib.nullcontext():
+ if result.shape[1] == 4: # This is the RGBA tensor from color variations or replication
+ if target_channels == 4:
+ # Keep as is - just 4 channels
+ # if debugger.enabled and debugger.debug_level >= 1:
+ # print(f"Using 4 channels for output (RGBA from color scheme or replication)")
+ # # No change to 'result' needed here.
+ pass
+
+ elif target_channels > 4:
+ num_base_channels = result.shape[1] # Should be 4
+ additional_channels_needed = target_channels - num_base_channels
+ additional_channels_list = []
+
+ # Parameters from the main get_domain_warp function scope:
+ # p, device, octaves (original), seed (original from get_domain_warp call),
+ # scale, warp_strength, phase_shift, time (original from get_domain_warp call)
+
+ base_channels_tensor = result # This is the initial RGBA
+
+ for i in range(additional_channels_needed):
+ current_channel_gen_index = i # for variation uniqueness: 0, 1, ... up to additional_channels_needed-1
+ variation_seed = seed + 700 + (current_channel_gen_index * 120) # Original seed from get_domain_warp
+ torch.manual_seed(variation_seed)
+
+ p_perturbed = p.clone()
+ pert_scale = 0.01 + (current_channel_gen_index * 0.005) # Slightly increasing perturbation
+ p_perturbed += (torch.randn_like(p) * pert_scale)
+ p_perturbed = torch.clamp(p_perturbed, 0.0, 1.0) # Keep coordinates in [0,1] range
+
+ time_offset = current_channel_gen_index * 0.03
+
+ # Vary parameters for the _domain_warp_with_phase call
+ varied_octaves_for_call = int(octaves + (current_channel_gen_index % 3) - 1)
+ varied_octaves_for_call = max(1, varied_octaves_for_call) # Ensure octaves >= 1
+
+ # These are the noise_type and warp_type args for _domain_warp_with_phase's internal logic
+ call_noise_type = current_channel_gen_index % 3 # Cycles 0,1,2 (FBM, Ridged, Billow for base)
+ call_warp_type = current_channel_gen_index % 4 # Cycles 0,1,2,3 (None, Offset, Dir, Swirl for warp)
+
+ varied_scale_factor = 1.0 + ((current_channel_gen_index % 5 - 2) * 0.05) # Approx +/- 10%
+ varied_warp_strength_factor = 1.0 + ((current_channel_gen_index % 7 - 3) * 0.05) # Approx +/- 15%
+ varied_phase_shift_val = phase_shift * (1.0 + ((current_channel_gen_index % 9 - 4) * 0.05)) # Approx +/- 20%
+ # Ensure phase_shift stays in a reasonable range (e.g., [0,1] if that's its typical input domain)
+ varied_phase_shift_clamped = torch.clamp(torch.tensor(varied_phase_shift_val), 0.0, 1.0).item()
+
+ new_channel_bhwc = DomainWarpGenerator._domain_warp_with_phase(
+ p_perturbed, device, varied_octaves_for_call, variation_seed,
+ call_noise_type, call_warp_type,
+ scale * varied_scale_factor,
+ warp_strength * varied_warp_strength_factor,
+ varied_phase_shift_clamped,
+ time + time_offset
+ ) # Output is [B,H,W,1] and in [0,1] range from _domain_warp_with_phase
+
+ new_channel_bchw = new_channel_bhwc.permute(0, 3, 1, 2) # [B, 1, H, W]
+ additional_channels_list.append(new_channel_bchw)
+
+ if additional_channels_list:
+ all_additional = torch.cat(additional_channels_list, dim=1)
+ result = torch.cat([base_channels_tensor, all_additional], dim=1)
+ # If no additional channels were actually generated (e.g. if additional_channels_needed was 0),
+ # result remains base_channels_tensor, which is correct.
+
+ # if debugger.enabled and debugger.debug_level >= 1:
+ # print(f"Expanded to {result.shape[1]} structured channels.")
+ # debugger.track_tensor_shape_history(result, f"result_{result.shape[1]}ch_structured", f"channel_expansion_structured_{result.shape[1]}")
+
+ elif target_channels < 4: # Target is 1, 2, or 3
+ # if debugger.enabled and debugger.debug_level >= 1:
+ # print(f"Reducing channels from 4 to {target_channels}")
+ result = result[:, :target_channels, :, :] # Slice from the RGBA result
+ # if debugger.enabled: # Add tracking for sliced result
+ # debugger.track_tensor_shape_history(result, f"result_{target_channels}ch_sliced", f"channel_slicing_{target_channels}")
+ # If target_channels == 4, result is already the correct 4-channel tensor.
+
+ # else: This case implies result.shape[1] was not 4 initially.
+ # This shouldn't happen if the preceding logic correctly produces a 4-channel 'result'
+ # (from color variations or replication of a single channel noise).
+ # If it does, the original code didn't explicitly handle it either beyond the try-except for channel expansion.
+
+ except Exception as e:
+ # if debugger.enabled:
+ # debugger.add_warning(f"Error expanding channels: {str(e)}", category="channel_expansion_error")
+ # print(f"โ ๏ธ Error expanding channels: {str(e)}")
+ # Ensure we have at least the base channels
+ if result.shape[1] != target_channels:
+ correct_shape = (batch_size, target_channels, height, width)
+ correct_result = torch.zeros(correct_shape, device=device)
+ # Copy available channels
+ channels_to_copy = min(result.shape[1], target_channels)
+ correct_result[:, :channels_to_copy] = result[:, :channels_to_copy]
+ # Fill remaining channels with noise if needed
+ if channels_to_copy < target_channels:
+ for c in range(channels_to_copy, target_channels):
+ correct_result[:, c] = torch.randn_like(result[:, 0])
+ result = correct_result
+ # print(f"โ Created fallback tensor with {target_channels} channels") # Removed this line
+
+ # Final tracking and analysis of the output tensor
+ # if debugger.enabled:
+ # debugger.track_tensor_shape_history(result, "final_result", "final_output")
+ # debugger.analyze_tensor(result, "domain_warp_final")
+ # if debugger.debug_level >= 1:
+ # print(f"โ Domain warp generated with shape {result.shape}")
+ # # Log tensor stats
+ # with torch.no_grad():
+ # print(f"๐ Tensor stats: min={result.min().item():.4f}, max={result.max().item():.4f}, "
+ # f"mean={result.mean().item():.4f}, std={result.std().item():.4f}")
+
+ return result
+
+ @staticmethod
+ def _domain_warp_with_phase(p, device, octaves, seed, noise_type, warp_type, scale, warp_strength, phase, time=0.0):
+ """
+ Generate domain warped noise with phase modulation
+
+ Args:
+ p: Input coordinates tensor [batch, height, width, 2]
+ device: Device to create tensor on
+ octaves: Number of octaves for FBM noise
+ seed: Random seed value
+ noise_type: Type of base noise to use
+ warp_type: Type of domain warping to apply
+ scale: Scale factor for input coordinates
+ warp_strength: Strength of the domain warping
+ phase: Value between 0 and 1 to blend between base and warped
+ time: Animation time value
+
+ Returns:
+ Warped noise tensor [batch, height, width, 1]
+ """
+ # Convert octaves to integer
+ octaves = int(octaves)
+
+ # For very small phases, return pure base noise
+ if phase < 0.01:
+ # Generate base noise
+ if noise_type == 0: # FBM
+ torch.manual_seed(seed)
+ # FIXED: Update the call to match the new function signature, passing time as t parameter
+ return DomainWarpGenerator.fbm_base(p * scale, octaves, seed, False, 0.8, 2.0, 0.5, time)
+ elif noise_type == 1: # Ridged
+ torch.manual_seed(seed)
+ return DomainWarpGenerator.ridged_multi(p * scale, octaves, device, seed, time)
+ else: # Billow
+ torch.manual_seed(seed)
+ return DomainWarpGenerator.billow(p * scale, octaves, device, seed, time)
+
+ # For higher phases, we blend between base noise and warped noise
+ # Generate base noise for later blending
+ if noise_type == 0: # FBM
+ torch.manual_seed(seed)
+ base_noise = DomainWarpGenerator.fbm_base(p * scale, octaves, seed, False, 0.8, 2.0, 0.5, time)
+ elif noise_type == 1: # Ridged
+ torch.manual_seed(seed)
+ base_noise = DomainWarpGenerator.ridged_multi(p * scale, octaves, device, seed, time)
+ else: # Billow
+ torch.manual_seed(seed)
+ base_noise = DomainWarpGenerator.billow(p * scale, octaves, device, seed, time)
+
+ # Generate domain warped noise
+ # Increase the warp strength for more visible effect
+ enhanced_warp_strength = warp_strength * (1.0 + phase * 2.0)
+
+ # Use a larger scale for warped noise to create more differentiation
+ warped_scale = scale * (1.0 + phase * 0.5)
+
+ # Use a different seed for the warped noise to ensure pattern difference
+ warp_seed = seed + 42
+
+ # Create a warped version with enhanced parameters
+ warped_noise = DomainWarpGenerator.domain_warp(
+ p, warp_type, warped_scale, enhanced_warp_strength, time,
+ octaves, device, warp_seed, False
+ )
+
+ # Blend between base and warped with enhanced contrast
+ phase_adjusted = phase ** 0.8 # Make the phase response more linear
+
+ # Apply contrast enhancement to warped noise for more difference
+ warped_enhanced = (warped_noise - 0.5) * (1.0 + phase * 0.5) + 0.5
+
+ # Blend with a method that preserves more detail
+ blended = base_noise * (1.0 - phase_adjusted) + warped_enhanced * phase_adjusted
+
+ # Add subtle detail enhancement based on the difference between base and warped
+ difference = torch.abs(warped_enhanced - base_noise)
+ blended = blended + difference * phase_adjusted * 0.2
+
+ # Normalize the result to ensure it stays in appropriate range
+ blended = torch.clamp(blended, 0.0, 1.0)
+
+ # Check if blended result is too similar to base noise (for debugging)
+ mean_diff = torch.mean(torch.abs(blended - base_noise)).item()
+
+ return blended
+
+ @staticmethod
+ def fbm_base(p, num_octaves, seed, temporal=False, h=0.8, lacunarity=2.0, gain=0.5, t=0.0):
+ """
+ Generate fractal Brownian motion base noise with enhanced structure
+
+ Args:
+ p: Input coordinates tensor [batch, height, width, dim]
+ num_octaves: Number of noise layers to combine (minimum 3 for good structure)
+ seed: Random seed for noise generation
+ temporal: Whether to include temporal dimension
+ h: Fractal dimension factor controlling roughness
+ lacunarity: Frequency multiplier between octaves
+ gain: Amplitude multiplier between octaves
+ t: Time value for temporal noise
+
+ Returns:
+ FBM noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, dim = p.shape
+
+ # Ensure minimum octaves for good structure and convert to integer
+ num_octaves = max(3, int(num_octaves))
+
+ # FIXED: Make sure seed is an integer, not a device
+ # If seed is a device object (which could happen due to parameter ordering issues),
+ # use a default seed value
+ if not isinstance(seed, (int, float, torch.Tensor)) or isinstance(seed, torch.device):
+ # print(f"WARNING: Invalid seed type ({type(seed)}), using default seed") # Removed this line
+ seed = 12345 # Use a default seed value
+
+ # Convert seed to integer if it's a tensor or float
+ if isinstance(seed, torch.Tensor):
+ seed = seed.item()
+ seed = int(seed)
+
+ # Add variation based on seed
+ seed_hash = seed % 10000
+
+ # Create seed-specific parameter variations
+ frequency_variation = 1.0 + (seed_hash % 100) / 200.0 # 0.5% - 1.5% variation
+ lacunarity_variation = lacunarity * (1.0 + (seed_hash % 73) / 500.0) # Slight variation
+ gain_variation = gain * (1.0 + (seed_hash % 83) / 400.0) # Slight variation
+
+ # Initialize result with slight bias for more distinctive patterns
+ result = torch.zeros(batch, height, width, 1, device=p.device)
+ bias = (seed_hash % 100) / 1000.0 - 0.05 # Small initial bias: -0.05 to 0.05
+ result += bias
+
+ amplitude = 1.0
+ frequency = 1.0 * frequency_variation
+
+ # Apply initial domain warping for improved structure
+ warp_strength = 0.5 + (seed_hash % 50) / 100.0 # 0.5 to 1.0
+ warp_frequency = 1.2 + (seed_hash % 80) / 100.0 # 1.2 to 2.0
+
+ # Generate initial domain warping
+ warp = DomainWarpGenerator.simplex_noise(p * warp_frequency, seed + 1234) * warp_strength
+
+ # Save original coordinates
+ p_original = p.clone()
+
+ # Different seeds for each octave to avoid pattern repetition
+ octave_seeds = [(seed + i * 1337) % 100000 for i in range(num_octaves)]
+
+ for i in range(num_octaves):
+ # Apply progressive domain warping for better structure
+ if i > 0:
+ warp_amt = 0.1 * (i / num_octaves) * warp
+ p = p_original + warp_amt
+
+ # Generate noise at current frequency
+ if temporal:
+ noise = DomainWarpGenerator.simplex_noise_3d(p * frequency, octave_seeds[i], t * frequency)
+ else:
+ noise = DomainWarpGenerator.simplex_noise(p * frequency, octave_seeds[i])
+
+ # Apply non-linear transformations for more structure
+ if i == 0:
+ # First octave - add ridges for backbone structure
+ noise = torch.abs(noise) * 2.0 - 1.0
+ elif i == 1:
+ # Second octave - enhance details with slight distortion
+ noise = noise * (1.0 + torch.sin(p[..., 0:1] * 3.0 + seed_hash) * 0.1)
+ elif i >= num_octaves - 2:
+ # Last octaves - add fine details
+ noise = noise * (1.0 + torch.cos(p[..., 1:2] * 2.0 + seed_hash + i) * 0.1)
+
+ # Add weighted noise to result
+ result += amplitude * noise
+
+ # Update parameters for next octave
+ frequency *= lacunarity_variation
+ amplitude *= gain_variation * (1.0 - 0.1 * (i / max(1, num_octaves - 1)))
+
+ # Normalize result to [-1, 1] range with enhanced contrast
+ result_min = torch.min(result)
+ result_max = torch.max(result)
+ result = 2.0 * (result - result_min) / (result_max - result_min) - 1.0
+
+ # Apply final non-linear transform for better visual structure
+ result = torch.tanh(result * 1.2) * 0.9
+
+ return result
+
+ @staticmethod
+ def simplex_noise(p, seed):
+ """
+ Generate simplex noise for input coordinates
+
+ Args:
+ p: Coordinate tensor [batch, height, width, dim] or other shape that can be reshaped
+ seed: Random seed for noise generation
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ # Handle different input shapes
+ original_shape = p.shape
+
+ # Check input tensor dimensions and reshape if necessary
+ if len(original_shape) == 4:
+ batch, height, width, dim = original_shape
+ elif len(original_shape) == 3:
+ # Assume [height, width, dim] format - add batch dimension
+ height, width, dim = original_shape
+ batch = 1
+ p = p.unsqueeze(0) # Add batch dimension
+ elif len(original_shape) == 5:
+ # Handle 5D tensor (e.g., from 3D noise with time dimension)
+ batch, height, width, depth, dim = original_shape
+ # Reshape to 4D by flattening height and width
+ p = p.reshape(batch, height * width, depth, dim)
+ height, width = height * width, depth
+ dim = original_shape[-1]
+ else:
+ # For any other shape, try to adapt
+ total_elements = p.numel()
+ dim = original_shape[-1] if len(original_shape) > 1 else 1
+
+ if dim < 1:
+ dim = 1
+
+ # Calculate remaining dimensions
+ remaining = total_elements // dim
+
+ # Default to square-ish dimensions if we need to reshape
+ width = int(math.sqrt(remaining))
+ height = remaining // width
+ batch = 1
+
+ # Reshape to 4D
+ p = p.reshape(batch, height, width, dim)
+
+ # Use seed to create pattern variations
+ seed_hash = seed % 10000
+ scale_variation = 1.0 + (seed_hash % 100) / 100.0 * 0.5 # Scale between 1.0 and 1.5
+ rotation_angle = (seed_hash % 628) / 100.0 # Angle between 0 and 6.28 (2ฯ)
+
+ # Simplex noise constants
+ F2 = 0.5 * (math.sqrt(3.0) - 1.0)
+ G2 = (3.0 - math.sqrt(3.0)) / 6.0
+
+ # Apply seed-based rotation and scaling to coordinates
+ # Create rotation matrix
+ cos_theta = torch.cos(torch.tensor(rotation_angle))
+ sin_theta = torch.sin(torch.tensor(rotation_angle))
+
+ # Make sure p has the right dimensions for coordinate operations
+ if p.shape[-1] < 2:
+ # If dim < 2, expand to at least 2 dimensions for x,y coordinates
+ if p.shape[-1] == 1:
+ # Duplicate the single dimension
+ p = torch.cat([p, p], dim=-1)
+ else:
+ # Add a zero dimension
+ zeros = torch.zeros_like(p[..., 0:1])
+ p = torch.cat([p, zeros], dim=-1)
+
+ # Apply rotation and scaling to input coordinates for more variation
+ x = p[..., 0:1].clone()
+ y = p[..., 1:2].clone()
+ p_rotated = torch.cat([
+ (x * cos_theta - y * sin_theta) * scale_variation,
+ (x * sin_theta + y * cos_theta) * scale_variation
+ ], dim=-1)
+
+ # Extract coordinates
+ x = p_rotated[..., 0:1]
+ y = p_rotated[..., 1:2]
+
+ # Skew coordinates to simplex space
+ s = (x + y) * F2
+ i = torch.floor(x + s)
+ j = torch.floor(y + s)
+
+ # Unskew back to regular space
+ t = (i + j) * G2
+ X0 = i - t
+ Y0 = j - t
+
+ # Calculate relative coordinates from cell origin
+ x0 = x - X0
+ y0 = y - Y0
+
+ # Determine simplex (triangle) we're in and corresponding vertices
+ # i1, j1 are the second closest corner
+ # i2, j2 are the farthest corner (always (1,1) relative to (i0,j0))
+ i1 = torch.zeros_like(x0)
+ j1 = torch.zeros_like(y0)
+
+ i1[x0 > y0] = 1.0
+ j1[x0 <= y0] = 1.0
+
+ # Coordinates of the other corners
+ x1 = x0 - i1 + G2
+ y1 = y0 - j1 + G2
+ x2 = x0 - 1.0 + 2.0 * G2
+ y2 = y0 - 1.0 + 2.0 * G2
+
+ # Hash the coordinates based on seed for consistent gradients
+ # This ensures different seeds produce different patterns
+ def hash_coords(ix, iy, seed):
+ # Better hashing function that uses seed
+ # Ensure all operations are tensor-compatible
+ h = ix * 1619 + iy * 31337 + seed * 2459
+ h = torch.fmod(h * h * h, 1013)
+ h = torch.fmod(h * h, 1013)
+ h = torch.fmod(h * h * h, 1013)
+ return h.type(torch.int64)
+
+ # Calculate contribution from each corner
+ def gradient(hash_val, x, y):
+ # Improved gradient selection for more structured patterns
+ # Use tensor operations instead of conditional statements
+ h = hash_val & 7
+
+ # Create all possible gradient vectors based on h value
+ grad_x = torch.zeros_like(x)
+ grad_y = torch.zeros_like(y)
+
+ # h == 0: return x + y
+ mask_0 = (h == 0)
+ grad_x = torch.where(mask_0, x + grad_x, grad_x)
+ grad_y = torch.where(mask_0, y + grad_y, grad_y)
+
+ # h == 1: return x - y
+ mask_1 = (h == 1)
+ grad_x = torch.where(mask_1, x + grad_x, grad_x)
+ grad_y = torch.where(mask_1, -y + grad_y, grad_y)
+
+ # h == 2: return -x + y
+ mask_2 = (h == 2)
+ grad_x = torch.where(mask_2, -x + grad_x, grad_x)
+ grad_y = torch.where(mask_2, y + grad_y, grad_y)
+
+ # h == 3: return -x - y
+ mask_3 = (h == 3)
+ grad_x = torch.where(mask_3, -x + grad_x, grad_x)
+ grad_y = torch.where(mask_3, -y + grad_y, grad_y)
+
+ # h == 4: return x + x
+ mask_4 = (h == 4)
+ grad_x = torch.where(mask_4, x * 2.0 + grad_x, grad_x)
+ grad_y = torch.where(mask_4, grad_y, grad_y)
+
+ # h == 5: return -x - x
+ mask_5 = (h == 5)
+ grad_x = torch.where(mask_5, -x * 2.0 + grad_x, grad_x)
+ grad_y = torch.where(mask_5, grad_y, grad_y)
+
+ # h == 6: return y + y
+ mask_6 = (h == 6)
+ grad_x = torch.where(mask_6, grad_x, grad_x)
+ grad_y = torch.where(mask_6, y * 2.0 + grad_y, grad_y)
+
+ # h == 7: return -y - y
+ mask_7 = (h == 7)
+ grad_x = torch.where(mask_7, grad_x, grad_x)
+ grad_y = torch.where(mask_7, -y * 2.0 + grad_y, grad_y)
+
+ return grad_x + grad_y
+
+ def corner_contribution(ix, iy, x, y):
+ hash_val = hash_coords(ix, iy, seed_hash)
+ grad = gradient(hash_val, x, y)
+
+ # Calculate falloff function (improved)
+ t = 0.5 - x*x - y*y
+ # Enhanced falloff for better structure
+ t = torch.maximum(t, torch.zeros_like(t))
+
+ # Apply steeper falloff for more defined features
+ t4 = t * t * t * t * t # t^5 for sharper features
+ return t4 * grad
+
+ # Calculate noise contribution from each corner
+ i0 = i.long()
+ j0 = j.long()
+ i1 = (i0 + i1.long()) % 1024 # Prevent overflow
+ j1 = (j0 + j1.long()) % 1024
+ i2 = (i0 + 1) % 1024
+ j2 = (j0 + 1) % 1024
+
+ # Compute contributions from three corners
+ n0 = corner_contribution(i0, j0, x0, y0)
+ n1 = corner_contribution(i1, j1, x1, y1)
+ n2 = corner_contribution(i2, j2, x2, y2)
+
+ # Sum up and scale to [-1,1]
+ # Apply non-linear transformation for better structure
+ noise = 70.0 * (n0 + n1 + n2)
+
+ # Apply contrast enhancement for more structured appearance
+ noise = torch.tanh(noise * 1.2) * 0.85
+
+ # Reshape back to original batch, height, width dimensions with single channel
+ if len(original_shape) == 3:
+ # Remove the batch dimension we added
+ noise = noise.squeeze(0)
+ elif len(original_shape) == 5:
+ # Reshape back to original 5D shape with single output channel
+ batch, height, width, depth, _ = original_shape
+ noise = noise.reshape(batch, height, width, depth, 1)
+
+ return noise.unsqueeze(-1) if noise.shape[-1] != 1 else noise
+
+ @staticmethod
+ def fbm_base_temporal(p, octaves, time, device, seed, use_temporal_coherence=True):
+ """
+ Generate FBM (Fractal Brownian Motion) noise with temporal coherence
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ octaves: Number of octaves
+ time: Animation time value
+ device: Device to create tensor on
+ seed: Base seed value (consistent across frames)
+ use_temporal_coherence: Whether to use true temporal coherence
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = p.shape
+
+ # Convert octaves to integer
+ octaves = int(octaves)
+
+ # FIXED: Make sure seed is an integer, not a device
+ # If seed is a device object (which could happen due to parameter ordering issues),
+ # use a default seed value
+ if not isinstance(seed, (int, float, torch.Tensor)) or isinstance(seed, torch.device):
+ # print(f"WARNING: Invalid seed type ({type(seed)}), using default seed") # Removed this line
+ seed = 12345 # Use a default seed value
+
+ # Convert seed to integer if it's a tensor or float
+ if isinstance(seed, torch.Tensor):
+ seed = seed.item()
+ seed = int(seed)
+
+ # Standard FBM using user-controlled octaves
+ result = torch.zeros(batch, height, width, 1, device=p.device) # Use p.device instead of device parameter
+ amp = 1.0
+ freq = 1.0
+
+ for i in range(min(octaves, 8)):
+ # Generate simplex noise at current frequency
+ current_p = p * freq
+
+ # Add time as a third dimension for temporal coherence
+ time_offset = time * (0.2 + i * 0.05) # Different rate per octave
+
+ # Create 3D coordinates with time
+ current_p_temporal = torch.cat([
+ current_p,
+ torch.ones_like(current_p[:, :, :, :1]) * time_offset
+ ], dim=-1)
+
+ # Generate 3D simplex noise with temporal coherence
+ noise = DomainWarpGenerator.simplex_noise_3d(current_p_temporal, seed + i)
+
+ # Add to result with current amplitude
+ result += amp * noise
+
+ # Prepare for next octave
+ freq *= 2.0
+ amp *= 0.5
+
+ return result
+
+ @staticmethod
+ def simplex_noise_3d(coords, seed=0):
+ """
+ 3D simplex noise implementation in PyTorch for temporal coherence
+ This provides coherent noise values when the third dimension is time
+
+ Args:
+ coords: Coordinate tensor [batch, height, width, 3] or other compatible shape
+ seed: Random seed value
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ # FIXED: Make sure seed is an integer, not a device
+ if not isinstance(seed, (int, float, torch.Tensor)) or isinstance(seed, torch.device):
+ # print(f"WARNING: Invalid seed type ({type(seed)}), using default seed") # Removed this line
+ seed = 12345 # Use a default seed value
+
+ # Convert seed to integer if it's a tensor or float
+ if isinstance(seed, torch.Tensor):
+ seed = seed.item()
+ seed = int(seed)
+
+ # Handle different input shapes
+ original_shape = coords.shape
+
+ # Check input tensor dimensions and reshape if necessary
+ if len(original_shape) == 4 and original_shape[-1] >= 3:
+ batch, height, width, _ = original_shape
+ elif len(original_shape) == 3:
+ # Assume [height, width, dim] format - add batch dimension
+ height, width, _ = original_shape
+ batch = 1
+ coords = coords.unsqueeze(0) # Add batch dimension
+ else:
+ # For any other shape, try to adapt
+ total_elements = coords.numel()
+ dim = original_shape[-1] if len(original_shape) > 1 else 3
+
+ if dim < 3:
+ # If less than 3 dims, pad with zeros
+ if len(original_shape) > 1:
+ padding_needed = 3 - original_shape[-1]
+ zeros = torch.zeros((*original_shape[:-1], padding_needed), device=coords.device)
+ coords = torch.cat([coords, zeros], dim=-1)
+ else:
+ # Handle 1D tensor
+ coords = coords.reshape(-1, 1)
+ zeros = torch.zeros(coords.shape[0], 2, device=coords.device)
+ coords = torch.cat([coords, zeros], dim=-1)
+
+ # Calculate remaining dimensions
+ total_elements = coords.numel()
+ remaining = total_elements // 3
+
+ # Default to square-ish dimensions if we need to reshape
+ width = int(math.sqrt(remaining))
+ height = remaining // width
+ batch = 1
+
+ # Reshape to 4D
+ coords = coords.reshape(batch, height, width, 3)
+
+ device = coords.device
+
+ # Set deterministic seed
+ torch.manual_seed(seed)
+
+ # Extract x, y, z (time) components
+ x, y, z = coords[:, :, :, 0], coords[:, :, :, 1], coords[:, :, :, 2]
+
+ # 3D Simplex noise constants
+ F3 = 1.0 / 3.0
+ G3 = 1.0 / 6.0
+
+ # Skew input space to determine which simplex cell contains point
+ s = (x + y + z) * F3
+ i = torch.floor(x + s)
+ j = torch.floor(y + s)
+ k = torch.floor(z + s)
+
+ t = (i + j + k) * G3
+ X0 = i - t
+ Y0 = j - t
+ Z0 = k - t
+
+ # Unskewed distances from cell origin
+ x0 = x - X0
+ y0 = y - Y0
+ z0 = z - Z0
+
+ # Determine simplex (tetrahedron) based on which of x0, y0, z0 is largest
+ # We need to rank the coordinates to determine which simplex we're in
+ # This is a simplified approach that works for most cases
+ x_ge_y = (x0 >= y0).float()
+ y_ge_z = (y0 >= z0).float()
+ x_ge_z = (x0 >= z0).float()
+
+ # Calculate offsets for second corner
+ i1 = (x_ge_y * x_ge_z).float()
+ j1 = ((1.0 - x_ge_y) * y_ge_z).float()
+ k1 = ((1.0 - x_ge_z) * (1.0 - y_ge_z)).float()
+
+ # Calculate offsets for third corner
+ i2 = (x_ge_y + (1.0 - x_ge_y) * x_ge_z).float()
+ j2 = (x_ge_y * (1.0 - x_ge_z) + (1.0 - x_ge_y)).float()
+ k2 = ((1.0 - x_ge_z) + x_ge_z * (1.0 - x_ge_y)).float()
+
+ # Calculate corner positions (in unskewed coordinates)
+ x1 = x0 - i1 + G3
+ y1 = y0 - j1 + G3
+ z1 = z0 - k1 + G3
+
+ x2 = x0 - i2 + 2.0 * G3
+ y2 = y0 - j2 + 2.0 * G3
+ z2 = z0 - k2 + 2.0 * G3
+
+ x3 = x0 - 1.0 + 3.0 * G3
+ y3 = y0 - 1.0 + 3.0 * G3
+ z3 = z0 - 1.0 + 3.0 * G3
+
+ # Hash coordinates of the corners to get random gradients
+ # This is a simplified hash function that works for visualization
+ def hash_coord(a, b, c, s):
+ h = 1664525 * a + 1013904223
+ h += 1664525 * b + 1013904223
+ h += 1664525 * c + 1013904223
+ h += s # Add seed for variation
+ h = torch.fmod(h, 3777.7777)
+
+ # Map hash to a unit vector (gradient)
+ theta = h * 3.883222 # ~pi*1.236 to cover the unit sphere
+ phi = h * 0.67899 # ~pi/5 for variation
+
+ # Create 3D gradient direction
+ gx = torch.cos(theta) * torch.cos(phi)
+ gy = torch.sin(theta) * torch.cos(phi)
+ gz = torch.sin(phi)
+ return torch.stack([gx, gy, gz], dim=-1)
+
+ # Get gradient vectors for each corner
+ g0 = hash_coord(i, j, k, seed)
+ g1 = hash_coord(i + i1, j + j1, k + k1, seed)
+ g2 = hash_coord(i + i2, j + j2, k + k2, seed)
+ g3 = hash_coord(i + 1.0, j + 1.0, k + 1.0, seed)
+
+ # Calculate noise contribution from each corner
+ def corner_influence(grad, x, y, z):
+ # Distance vector from corner to point
+ t = 0.6 - x*x - y*y - z*z
+ t = torch.maximum(t, torch.zeros_like(t))
+ t4 = t*t*t*t
+
+ # Gradient dot product with vertex->point vector
+ n = grad[:,:,:,0]*x + grad[:,:,:,1]*y + grad[:,:,:,2]*z
+ return t4 * n
+
+ # Compute contributions from the four corners
+ n0 = corner_influence(g0, x0, y0, z0)
+ n1 = corner_influence(g1, x1, y1, z1)
+ n2 = corner_influence(g2, x2, y2, z2)
+ n3 = corner_influence(g3, x3, y3, z3)
+
+ # Sum contributions and scale to match 2D output range
+ noise = (n0 + n1 + n2 + n3) * 30.0
+
+ # Reshape back to original dimensions if needed
+ if len(original_shape) == 3:
+ # Remove the batch dimension we added
+ noise = noise.squeeze(0)
+
+ return noise.unsqueeze(-1) if noise.shape[-1] != 1 else noise
+
+ @staticmethod
+ def domain_warp(p, warp_type=0, scale=1.0, warp_strength=0.5, time=0.0, octaves=3, device=None, seed=0, use_temporal_coherence=False):
+ """
+ Generate domain warped noise
+
+ Args:
+ p: Input coordinates tensor [batch, height, width, 2]
+ warp_type: Type of domain warping (0-3)
+ scale: Scale factor for input coordinates
+ warp_strength: Strength of the warping effect
+ time: Animation time parameter
+ octaves: Number of octaves for fractal noise
+ device: Device to create tensor on
+ seed: Random seed for noise generation
+ use_temporal_coherence: Whether to use temporal coherence for animation
+
+ Returns:
+ Warped noise tensor [batch, height, width, 1]
+ """
+ # Ensure device is set
+ if device is None:
+ device = p.device
+
+ # Set torch seed to ensure consistent noise generation
+ torch.manual_seed(seed)
+
+ # Convert warp_type to int if needed
+ warp_type = int(warp_type)
+
+ # Clamp to valid range
+ warp_type = max(0, min(warp_type, 3))
+
+ # Convert octaves to integer
+ octaves = int(octaves)
+
+ # For warp_type 0 (no warping), return base FBM noise
+ if warp_type == 0:
+ # Updated to use the new function signature
+ return DomainWarpGenerator.fbm_base(p * scale, octaves, seed, use_temporal_coherence, 0.8, 2.0, 0.5, time)
+
+ # For other warp types, generate base noise first
+ torch.manual_seed(seed)
+ base_noise = DomainWarpGenerator.fbm_base(p * scale, octaves, seed, use_temporal_coherence, 0.8, 2.0, 0.5, time)
+
+ # Create warped coordinates
+ warp_seed = seed + 42
+ torch.manual_seed(warp_seed)
+
+ # Generate warp vector field
+ if warp_type == 1:
+ # Simple offset warping
+ warp_coords = p * scale * 0.5
+ warp_noise = DomainWarpGenerator.simplex_noise(warp_coords, warp_seed) * warp_strength
+ elif warp_type == 2:
+ # Directional warping
+ warp_coords_x = p * scale * 0.4 + torch.tensor([[[[0.0, 0.5]]]], device=device)
+ warp_coords_y = p * scale * 0.4 + torch.tensor([[[[0.5, 0.0]]]], device=device)
+ warp_noise_x = DomainWarpGenerator.simplex_noise(warp_coords_x, warp_seed) * warp_strength
+ warp_noise_y = DomainWarpGenerator.simplex_noise(warp_coords_y, warp_seed + 17) * warp_strength
+ warp_noise = torch.cat([warp_noise_x, warp_noise_y], dim=-1)
+ else: # warp_type == 3, swirl warping
+ # Calculate distance from center and angle for swirl
+ center = torch.tensor([[[[0.0, 0.0]]]], device=device)
+ offset = p - center
+ dist = torch.sqrt(torch.sum(offset * offset, dim=-1, keepdim=True))
+ angle = torch.atan2(offset[..., 1:2], offset[..., 0:1])
+
+ # Create swirl effect
+ swirl = angle + dist * warp_strength * 5.0
+ warp_x = torch.cos(swirl) * dist
+ warp_y = torch.sin(swirl) * dist
+ warp_noise = torch.cat([warp_x, warp_y], dim=-1) - offset
+
+ # Apply warp to get new coordinates
+ p_warped = p + warp_noise
+
+ # Generate noise with warped coordinates
+ warped_noise = DomainWarpGenerator.fbm_base(
+ p_warped * scale, octaves, warp_seed, use_temporal_coherence, 0.8, 2.0, 0.5, time
+ )
+
+ # Blend base and warped noise
+ blended = (base_noise + warped_noise) * 0.5
+
+ # Add detail enhancement
+ detail = torch.abs(warped_noise - base_noise) * 0.5
+ result = blended + detail
+
+ return torch.clamp(result, -1.0, 1.0)
+
+ @staticmethod
+ def ridged_multi(p, num_octaves, device, seed, t=0.0):
+ """
+ Generate ridged multifractal noise with enhanced structure
+
+ Args:
+ p: Input coordinates tensor [batch, height, width, dim]
+ num_octaves: Number of noise layers to combine (minimum 3 for good structure)
+ device: Device for computation (not used directly, kept for API compatibility)
+ seed: Random seed for noise generation
+ t: Time value for temporal noise
+
+ Returns:
+ Ridged multifractal noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, dim = p.shape
+
+ # Ensure minimum octaves for good structure and convert to integer
+ num_octaves = max(3, int(num_octaves))
+
+ # Handle seed type issues
+ if not isinstance(seed, (int, float, torch.Tensor)) or isinstance(seed, torch.device):
+ # print(f"WARNING: Invalid seed type ({type(seed)}), using default seed") # Removed this line
+ seed = 12345
+
+ # Convert seed to integer
+ if isinstance(seed, torch.Tensor):
+ seed = seed.item()
+ seed = int(seed)
+
+ # Add variation based on seed
+ seed_hash = seed % 10000
+
+ # Create seed-specific parameter variations
+ frequency_variation = 1.0 + (seed_hash % 100) / 200.0
+ lacunarity = 2.0 + (seed_hash % 73) / 500.0
+ gain = 0.5 * (1.0 + (seed_hash % 83) / 400.0)
+
+ # Initialize result with slight bias
+ result = torch.zeros(batch, height, width, 1, device=p.device)
+ bias = (seed_hash % 100) / 1000.0 - 0.05
+ result += bias
+
+ # Initial frequency and amplitude
+ frequency = 1.0 * frequency_variation
+ amplitude = 1.0
+
+ # Apply initial domain warping for improved structure
+ warp_strength = 0.5 + (seed_hash % 50) / 100.0
+ warp_frequency = 1.2 + (seed_hash % 80) / 100.0
+ warp = DomainWarpGenerator.simplex_noise(p * warp_frequency, seed + 1234) * warp_strength
+
+ # Save original coordinates
+ p_original = p.clone()
+
+ # Different seeds for each octave
+ octave_seeds = [(seed + i * 1337) % 100000 for i in range(num_octaves)]
+
+ # Weight values sum
+ sum_weights = 0.0
+
+ for i in range(num_octaves):
+ # Apply progressive domain warping for better structure
+ if i > 0:
+ warp_amt = 0.1 * (i / num_octaves) * warp
+ p = p_original + warp_amt
+
+ # Generate noise at current frequency
+ noise = DomainWarpGenerator.simplex_noise(p * frequency, octave_seeds[i])
+
+ # Ridged multifractal: abs(noise) * 2 - 1
+ noise = torch.abs(noise) * 2.0 - 1.0
+
+ # Invert and raise to a power to create ridges
+ noise = 1.0 - torch.abs(noise)
+ noise = torch.pow(noise, 2.5 + (i * 0.1))
+
+ # Apply special enhancements for certain octaves
+ if i == 0:
+ # First octave - enhance ridge sharpness
+ noise = noise * 1.25
+ elif i == 1:
+ # Second octave - add turbulence
+ noise = noise * (1.0 + torch.sin(p[..., 0:1] * 3.0 + seed_hash) * 0.1)
+ elif i >= num_octaves - 2:
+ # Last octaves - add fine ridge details
+ noise = noise * (1.0 + torch.cos(p[..., 1:2] * 4.0 + seed_hash + i) * 0.15)
+
+ # Add weighted noise to result
+ weight = amplitude
+ result += weight * noise
+ sum_weights += weight
+
+ # Update parameters for next octave
+ frequency *= lacunarity
+ amplitude *= gain * (1.0 - 0.15 * (i / max(1, num_octaves - 1)))
+
+ # Normalize by sum of weights
+ if sum_weights > 0:
+ result /= sum_weights
+
+ # Apply final non-linear transform for better visual structure
+ result = torch.tanh(result * 1.5) * 0.95
+
+ return result
+
+ @staticmethod
+ def billow(p, num_octaves, device, seed, t=0.0):
+ """
+ Generate billow noise with enhanced structure
+
+ Args:
+ p: Input coordinates tensor [batch, height, width, dim]
+ num_octaves: Number of noise layers to combine (minimum 3 for good structure)
+ device: Device for computation (not used directly, kept for API compatibility)
+ seed: Random seed for noise generation
+ t: Time value for temporal noise
+
+ Returns:
+ Billow noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, dim = p.shape
+
+ # Ensure minimum octaves for good structure and convert to integer
+ num_octaves = max(3, int(num_octaves))
+
+ # Handle seed type issues
+ if not isinstance(seed, (int, float, torch.Tensor)) or isinstance(seed, torch.device):
+ # print(f"WARNING: Invalid seed type ({type(seed)}), using default seed") # Removed this line
+ seed = 12345
+
+ # Convert seed to integer
+ if isinstance(seed, torch.Tensor):
+ seed = seed.item()
+ seed = int(seed)
+
+ # Add variation based on seed
+ seed_hash = seed % 10000
+
+ # Create seed-specific parameter variations
+ frequency_variation = 1.0 + (seed_hash % 100) / 200.0
+ lacunarity = 2.0 + (seed_hash % 73) / 500.0
+ gain = 0.5 * (1.0 + (seed_hash % 83) / 400.0)
+
+ # Initialize result with slight bias
+ result = torch.zeros(batch, height, width, 1, device=p.device)
+ bias = (seed_hash % 100) / 1000.0 - 0.05
+ result += bias
+
+ # Initial frequency and amplitude
+ frequency = 1.0 * frequency_variation
+ amplitude = 1.0
+
+ # Apply initial domain warping for improved structure
+ warp_strength = 0.5 + (seed_hash % 50) / 100.0
+ warp_frequency = 1.2 + (seed_hash % 80) / 100.0
+ warp = DomainWarpGenerator.simplex_noise(p * warp_frequency, seed + 1234) * warp_strength
+
+ # Save original coordinates
+ p_original = p.clone()
+
+ # Different seeds for each octave
+ octave_seeds = [(seed + i * 1337) % 100000 for i in range(num_octaves)]
+
+ # Weight values sum
+ sum_weights = 0.0
+
+ for i in range(num_octaves):
+ # Apply progressive domain warping for better structure
+ if i > 0:
+ warp_amt = 0.1 * (i / num_octaves) * warp
+ p = p_original + warp_amt
+
+ # Generate noise at current frequency
+ noise = DomainWarpGenerator.simplex_noise(p * frequency, octave_seeds[i])
+
+ # Billow noise: 2 * |noise| - 1
+ noise = 2.0 * torch.abs(noise) - 1.0
+
+ # Apply special enhancements for certain octaves
+ if i == 0:
+ # First octave - enhance main structure
+ noise = noise * 1.2
+ elif i == 1:
+ # Second octave - add variation
+ noise = noise * (1.0 + torch.sin(p[..., 0:1] * 2.5 + seed_hash) * 0.15)
+ elif i >= num_octaves - 2:
+ # Last octaves - add fine details
+ detail_factor = 0.2 + 0.1 * (i - (num_octaves - 2)) / 2.0
+ noise = noise * (1.0 + torch.cos(p[..., 1:2] * 3.5 + seed_hash + i) * detail_factor)
+
+ # Add weighted noise to result
+ weight = amplitude
+ result += weight * noise
+ sum_weights += weight
+
+ # Update parameters for next octave
+ frequency *= lacunarity
+ amplitude *= gain * (1.0 - 0.1 * (i / max(1, num_octaves - 1)))
+
+ # Normalize by sum of weights
+ if sum_weights > 0:
+ result /= sum_weights
+
+ # Apply final non-linear transform for better visual structure
+ result = torch.tanh(result * 1.3) * 0.9
+
+ return result
+
+def enhance_domain_warp_for_blending(shader_params):
+ """
+ Enhance domain warp parameters for more effective blending
+
+ Args:
+ shader_params: The shader parameters dictionary
+
+ Returns:
+ Enhanced shader parameters
+ """
+ # Do not modify user-specified parameters; return a copy.
+ return shader_params.copy() if shader_params else {}
+
+def get_domain_warp(self, batch_size, height, width, shader_params=None, device="cpu", seed=None, use_temporal_coherence=False):
+ """
+ Get a domain warp noise tensor
+
+ Args:
+ batch_size: Batch size
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Dictionary of shader parameters
+ device: Device to create tensor on
+ seed: Random seed
+ use_temporal_coherence: Whether to use temporal coherence
+
+ Returns:
+ Domain warp noise tensor
+ """
+ # Extract and validate parameters
+ params = shader_params.copy() if shader_params else {}
+
+ # Extract seed variation if provided
+ seed_variation = params.get('seed_variation', 0)
+ if seed_variation > 0:
+ # Apply seed variation to make patterns more distinctive
+ if seed is not None:
+ seed = seed + seed_variation
+ # print(f"Using seed variation: {seed_variation} for more distinctive patterns") # Removed this line
+
+ # Ensure minimum effective parameters for visual impact
+ warp_strength = max(params.get('warp_strength', 0.5), 0.8)
+ scale = max(params.get('scale', 1.0), 1.5)
+ octaves = max(params.get('octaves', 1), 3)
+ time = params.get('time', 0.0)
+ phase_shift = params.get('phase_shift', 0.5)
+
+ # Determine current seed based on temporal coherence
+ current_seed = seed if not use_temporal_coherence else None
+
+ # Generate the domain warp tensor
+ warp_tensor = generate_domain_warp_tensor(
+ batch_size, height, width, params,
+ scale, warp_strength, phase_shift, time,
+ device, current_seed, octaves,
+ use_temporal_coherence
+ )
+
+ return warp_tensor
+
+def generate_domain_warp_tensor(batch_size, height, width, shader_params=None, scale=1.0, warp_strength=0.5,
+ phase_shift=0.5, time=0.0, device="cuda", seed=0, octaves=4,
+ use_temporal_coherence=False, target_channels=None):
+ """
+ Directly generate a domain warp tensor without using ShaderToTensor
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Dictionary of shader parameters (overrides individual parameters if provided)
+ scale: Scale factor for coordinates
+ warp_strength: Strength of domain warping
+ phase_shift: Phase shift parameter
+ time: Animation time value
+ device: Device to create tensor on
+ seed: Random seed value
+ octaves: Number of octaves for FBM noise
+ use_temporal_coherence: Whether to use temporal coherence
+ target_channels: Number of output channels (4, 6, or 9) - helpful for different models
+
+ Returns:
+ Tensor with shape [batch_size, channels, height, width]
+ """
+ # Get debugger instance
+ # debugger = get_debugger()
+
+ # Safety check: ensure shader_params is a dictionary or create a default one
+ if shader_params is None or not isinstance(shader_params, dict):
+ if not isinstance(shader_params, dict) and shader_params is not None:
+ print(f"Warning: shader_params is not a dictionary ({type(shader_params)}), using defaults")
+ shader_params = {
+ "scale": scale,
+ "warp_strength": warp_strength,
+ "phase_shift": phase_shift,
+ "octaves": octaves,
+ "time": time,
+ "temporal_coherence": use_temporal_coherence,
+ "base_seed": seed if use_temporal_coherence else None,
+ }
+
+ # Add target_channels to shader_params if provided
+ if target_channels is not None:
+ shader_params["target_channels"] = target_channels
+ # if debugger.enabled and debugger.debug_level >= 1:
+ # print(f"Setting target_channels={target_channels} in shader_params")
+
+ # Log generation parameters
+ # if debugger.enabled and debugger.debug_level >= 1:
+ # debugger.log_generation_operation(
+ # "generate_domain_warp_tensor_start",
+ # {}, # empty dict for input_tensors since we don't have actual tensors yet
+ # torch.zeros((1, 1, 1, 1), device=device), # dummy tensor for output
+ # {
+ # "batch_size": batch_size,
+ # "height": height,
+ # "width": width,
+ # "scale": scale,
+ # "warp_strength": warp_strength,
+ # "phase_shift": phase_shift,
+ # "seed": seed,
+ # "octaves": octaves,
+ # "target_channels": target_channels,
+ # "shader_params": shader_params
+ # }
+ # )
+
+ # Add colorScheme if not present to make output more visually distinct
+ if "colorScheme" not in shader_params:
+ shader_params["colorScheme"] = "viridis"
+ shader_params["shaderColorIntensity"] = 0.9
+ # print("Adding color scheme for more visually distinct output") # Removed this line
+
+ # Ensure seed creates distinctive patterns (add some prime number offsets)
+ try:
+ # with debugger.time_operation("seed_transformation") if debugger.enabled else contextlib.nullcontext():
+ if shader_params.get("temporal_coherence", use_temporal_coherence):
+ # For temporal coherence, keep base_seed consistent
+ base_seed = shader_params.get("base_seed", seed)
+ shader_params["base_seed"] = base_seed
+
+ # Ensure each frame has a distinctive enough seed variation
+ shader_params["seed_variation"] = (seed % 100) * 73 + 17
+ # print(f"Using temporal coherence with base_seed={base_seed}, variation={shader_params['seed_variation']}") # Removed this line
+ else:
+ pass # Add pass to ensure the else block is not empty
+ # print(f"Using seed={seed} directly for non-temporal coherence (for pattern consistency).") # Removed this line
+ # The `seed` variable that is passed to DomainWarpGenerator.get_domain_warp
+ # will be the original incoming seed from the arguments of this function.
+ except Exception as e:
+ # if debugger.enabled:
+ # debugger.add_warning(f"Error in seed transformation: {str(e)}", category="seed_error")
+ pass
+
+ # Generate domain warp noise
+ try:
+ result = DomainWarpGenerator.get_domain_warp(
+ batch_size, height, width, shader_params, device, seed
+ )
+ except Exception as e:
+ # print(f"โ Error generating domain warp tensor: {str(e)}") # Removed this line
+ # Fallback to random noise with correct shape and channel count
+ target_ch = shader_params.get("target_channels", 9)
+ result = torch.randn((batch_size, target_ch, height, width), device=device)
+ # print(f"โ Created fallback tensor with {target_ch} channels and shape {result.shape}") # Removed this line
+
+ # Final analysis of the output tensor
+ # if debugger.enabled:
+ # debugger.track_tensor_shape_history(result, "final_result", "final_output")
+ # debugger.analyze_tensor(result, "domain_warp_final_tensor")
+ # if debugger.debug_level >= 1:
+ # print(f"โ Domain warp tensor generated with shape {result.shape}")
+ # # Log tensor stats
+ # with torch.no_grad():
+ # print(f"๐ Tensor stats: min={result.min().item():.4f}, max={result.max().item():.4f}, "
+ # f"mean={result.mean().item():.4f}, std={result.std().item():.4f}")
+
+ return result
+
+def get_rgb_noise(p, device, octaves, seed, noise_type, warp_type, scale, warp_strength, intensity, time):
+ """
+ Generate RGB noise with domain warping for colored textures
+
+ Args:
+ p: Input coordinates tensor [batch, height, width, 2]
+ device: Device to use for tensor operations
+ octaves: Number of octaves for noise
+ seed: Random seed
+ noise_type: Type of base noise (0: FBM, 1: Ridged, 2: Billow)
+ warp_type: Type of domain warping (0: None, 1-3: Different warp types)
+ scale: Scale factor for input coordinates
+ warp_strength: Strength of the domain warping
+ intensity: Intensity of the color variation
+ time: Animation time parameter
+
+ Returns:
+ RGB noise tensor [batch, height, width, 3]
+ """
+ # Create color channel variations
+ batch, h, w, _ = p.shape
+
+ # Intensity factor controls variation between channels
+ intensity_factor = intensity * 0.5
+
+ # Different coordinate sets for each channel
+ p_r = p.clone()
+ p_g = p.clone() + torch.tensor([[[[12.9898, 78.233]]]], device=device) * 0.0001
+ p_b = p.clone() + torch.tensor([[[[39.346, 11.135]]]], device=device) * 0.0001
+
+ # Generate base red channel
+ r_warp_result = DomainWarpGenerator._domain_warp_with_phase(
+ p_r, device, octaves, seed, noise_type, warp_type,
+ scale,
+ warp_strength,
+ 1.0, time
+ )
+
+ # Generate warped patterns with contrasting parameters
+ g_warp_result = DomainWarpGenerator._domain_warp_with_phase(
+ p_g, device, octaves, seed, noise_type, (warp_type + 1) % 4,
+ scale * 1.1,
+ warp_strength * (1.0 - 0.3 * intensity_factor),
+ 1.0, time + 0.1
+ )
+
+ b_warp_result = DomainWarpGenerator._domain_warp_with_phase(
+ p_b, device, octaves, seed, noise_type, (warp_type + 2) % 4,
+ scale * 0.9,
+ warp_strength * (1.0 + 0.3 * intensity_factor),
+ 1.0, time + 0.2
+ )
+
+ # Combine results into RGB noise tensor
+ rgb_noise = torch.stack([r_warp_result, g_warp_result, b_warp_result], dim=1)
+
+ return rgb_noise
+
+def add_domain_warp_to_tensor(tensor, shader_params=None, scale=1.0, warp_strength=0.5,
+ phase_shift=0.5, time=0.0, seed=0, octaves=4,
+ blend_mode="multiply", blend_factor=0.8, device=None):
+ """
+ Add domain warp noise to an existing tensor for texture enhancement
+
+ Args:
+ tensor: Input tensor to enhance [batch, channels, height, width]
+ shader_params: Dictionary of shader parameters (overrides individual parameters if provided)
+ scale: Scale factor for coordinates
+ warp_strength: Strength of domain warping
+ phase_shift: Phase shift parameter
+ time: Animation time value
+ seed: Random seed value
+ octaves: Number of octaves for noise
+ blend_mode: How to blend the noise with the original tensor ("add", "multiply", "overlay", "alpha")
+ blend_factor: How much of the effect to apply (0.0 to 1.0)
+ device: Device to create noise on (defaults to tensor's device)
+
+ Returns:
+ Enhanced tensor with domain warp applied
+ """
+ # Check if tensor is a class type (for registration with ShaderToTensor)
+ if isinstance(tensor, type):
+ # Register method with the class
+ @classmethod
+ def domain_warp(cls, p, warp_type=0, scale=1.0, warp_strength=0.5, time=0.0, octaves=3, device=None, seed=0, use_temporal_coherence=False):
+ """Apply domain warping to coordinates"""
+ if device is None:
+ device = p.device
+
+ # Get tensor dimensions
+ batch, height, width, _ = p.shape
+
+ # Generate domain warped noise
+ return DomainWarpGenerator.domain_warp(
+ p, warp_type, scale, warp_strength, time,
+ octaves, device, seed, use_temporal_coherence
+ )
+
+ # Add domain_warp method to the class
+ setattr(tensor, "domain_warp", domain_warp)
+
+ # Add apply_domain_warp method for blending with existing tensor
+ @classmethod
+ def apply_domain_warp(cls, tensor, shader_params=None, scale=1.0, warp_strength=0.5,
+ phase_shift=0.5, time=0.0, seed=0, octaves=4,
+ blend_mode="multiply", blend_factor=0.8, device=None):
+ """Add domain warp noise to a tensor"""
+ if device is None:
+ device = tensor.device
+
+ # Get tensor dimensions
+ batch, channels, height, width = tensor.shape
+
+ # Generate domain warp tensor
+ warp_tensor = generate_domain_warp_tensor(
+ batch, height, width, shader_params,
+ scale, warp_strength, phase_shift, time,
+ device, seed, octaves,
+ target_channels=channels # Pass the target channels
+ )
+
+ # Ensure compatible dimensions
+ if warp_tensor.shape[1] == 4 and channels != 4:
+ # Use only the first 'channels' dimensions of the warp_tensor
+ warp_tensor = warp_tensor[:, :channels]
+ elif warp_tensor.shape[1] < channels:
+ # Duplicate the last channel to match dimensions
+ last_channel = warp_tensor[:, -1:].repeat(1, channels - warp_tensor.shape[1], 1, 1)
+ warp_tensor = torch.cat([warp_tensor, last_channel], dim=1)
+
+ # Apply blend mode
+ if blend_mode == "add":
+ result = tensor + warp_tensor * blend_factor
+ elif blend_mode == "multiply":
+ normalized_warp = warp_tensor * 0.5
+ result = tensor * (1.0 + normalized_warp * blend_factor)
+ elif blend_mode == "overlay":
+ tensor_norm = tensor * 2.0 - 1.0
+ warp_norm = warp_tensor * 2.0 - 1.0
+
+ dark_mask = (tensor_norm < 0)
+ light_mask = (tensor_norm >= 0)
+
+ result = torch.zeros_like(tensor)
+ result[dark_mask] = (tensor_norm[dark_mask] * (1.0 + warp_norm[dark_mask] * blend_factor))
+ result[light_mask] = tensor_norm[light_mask] + warp_norm[light_mask] * blend_factor * (1.0 - tensor_norm[light_mask])
+
+ result = (result + 1.0) * 0.5
+ elif blend_mode == "alpha":
+ result = tensor * (1.0 - blend_factor) + warp_tensor * blend_factor
+ else:
+ result = tensor * (1.0 - blend_factor) + warp_tensor * blend_factor
+
+ # Ensure result is in valid range
+ result = torch.clamp(result, 0.0, 1.0)
+
+ return result
+
+ # Add the method to the class
+ setattr(tensor, "apply_domain_warp", apply_domain_warp)
+ # print("=== Added domain_warp functionality to ShaderToTensor ===") # Removed this line
+ return tensor
+
+ # Regular tensor processing
+ if device is None:
+ device = tensor.device
+
+ # Get tensor dimensions
+ batch, channels, height, width = tensor.shape
+
+ # Generate domain warp tensor
+ warp_tensor = generate_domain_warp_tensor(
+ batch, height, width, shader_params,
+ scale, warp_strength, phase_shift, time,
+ device, seed, octaves,
+ target_channels=channels # Pass the target channels
+ )
+
+ # Ensure compatible dimensions
+ if warp_tensor.shape[1] == 4 and channels != 4:
+ # Use only the first 'channels' dimensions of the warp_tensor
+ warp_tensor = warp_tensor[:, :channels]
+ elif warp_tensor.shape[1] < channels:
+ # Duplicate the last channel to match dimensions
+ last_channel = warp_tensor[:, -1:].repeat(1, channels - warp_tensor.shape[1], 1, 1)
+ warp_tensor = torch.cat([warp_tensor, last_channel], dim=1)
+
+ # Apply blend mode
+ if blend_mode == "add":
+ # Add mode: tensor + warp_tensor * blend_factor
+ result = tensor + warp_tensor * blend_factor
+ elif blend_mode == "multiply":
+ # Multiply mode: tensor * (1 + warp_tensor * blend_factor)
+ # Normalize warp_tensor to [-0.5, 0.5] for better multiplication effect
+ normalized_warp = warp_tensor * 0.5
+ result = tensor * (1.0 + normalized_warp * blend_factor)
+ elif blend_mode == "overlay":
+ # Overlay mode: blend between multiply and screen modes
+ # For dark areas in original, multiply; for light areas, screen
+ tensor_norm = tensor * 2.0 - 1.0 # Normalize to [-1, 1]
+ warp_norm = warp_tensor * 2.0 - 1.0 # Normalize to [-1, 1]
+
+ # Overlay formula: if base < 0.5, 2 * base * blend; else 1 - 2 * (1 - base) * (1 - blend)
+ dark_mask = (tensor_norm < 0)
+ light_mask = (tensor_norm >= 0)
+
+ # Apply overlay formula
+ result = torch.zeros_like(tensor)
+ result[dark_mask] = (tensor_norm[dark_mask] * (1.0 + warp_norm[dark_mask] * blend_factor))
+ result[light_mask] = tensor_norm[light_mask] + warp_norm[light_mask] * blend_factor * (1.0 - tensor_norm[light_mask])
+
+ # Normalize back to [0, 1]
+ result = (result + 1.0) * 0.5
+ elif blend_mode == "alpha":
+ # Alpha blend: lerp(tensor, warp_tensor, blend_factor)
+ result = tensor * (1.0 - blend_factor) + warp_tensor * blend_factor
+ else:
+ # Default to alpha blend if unknown mode
+ result = tensor * (1.0 - blend_factor) + warp_tensor * blend_factor
+
+ # Ensure result is in valid range
+ result = torch.clamp(result, 0.0, 1.0)
+
+ return result
\ No newline at end of file
diff --git a/shaders/temporal_coherent_noise.py b/shaders/temporal_coherent_noise.py
new file mode 100644
index 0000000..bc624ca
--- /dev/null
+++ b/shaders/temporal_coherent_noise.py
@@ -0,0 +1,562 @@
+import torch
+import math
+from ..shader_params_reader import ShaderParamsReader
+
+class TemporalCoherentNoiseGenerator:
+ """
+ Implementation of temporally coherent noise that maintains consistency
+ between animation frames by treating time as a proper 4th dimension
+ rather than using different seeds per frame.
+
+ This class generates noise that smoothly transitions between frames
+ by implementing true 4D (x,y,z,time) noise functions.
+ """
+
+ @staticmethod
+ def get_temporal_noise(batch_size, height, width, shader_params, device="cuda", base_seed=0):
+ """
+ Generate temporally coherent noise tensor
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Dictionary containing parameters from shader_params.json
+ device: Device to create tensor on
+ base_seed: Base seed value for initialization (NOT changed per frame)
+
+ Returns:
+ Tensor with shape [batch_size, 4, height, width]
+ """
+ # Extract parameters from shader_params
+ scale = shader_params.get("shaderScale", shader_params.get("scale", 1.0))
+ warp_strength = shader_params.get("shaderWarpStrength", shader_params.get("warp_strength", 0.5))
+ phase_shift = shader_params.get("shaderPhaseShift", shader_params.get("phase_shift", 0.5))
+ octaves = shader_params.get("shaderOctaves", shader_params.get("octaves", 3))
+ time = shader_params.get("time", 0.0) # Critical for temporal coherence
+ frequency_range = shader_params.get("frequencyRange", shader_params.get("frequency_range", 0))
+ shape_type = shader_params.get("shaderShapeType", shader_params.get("shape_type", "none"))
+ shape_mask_strength = shader_params.get("shaderShapeStrength", shader_params.get("shapemaskstrength", 1.0))
+
+ # Create coordinate grid (normalized to [-1, 1])
+ # Important: make sure we create the grid with the correct orientation for height/width
+ # In PyTorch, meshgrid with ij indexing puts height first, width second
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Verify p has the correct shape before proceeding
+ if p.shape[1:3] != (height, width):
+ # Try to fix by transposing or reshaping if needed
+ if p.shape[1] == width and p.shape[2] == height:
+ p = p.permute(0, 2, 1, 3)
+
+ # Set consistent seed for initialization
+ torch.manual_seed(base_seed)
+
+ # Generate temporal coherent noise
+ result = TemporalCoherentNoiseGenerator.temporal_spectral_noise(
+ p, scale, warp_strength, phase_shift, octaves,
+ frequency_range, time, device, base_seed
+ )
+
+ # Apply shape mask if requested
+ if shape_type not in ["none", "0"] and shape_mask_strength > 0:
+ shape_mask = ShaderParamsReader.apply_shape_mask(p, shape_type)
+ result = torch.lerp(result, result * shape_mask, shape_mask_strength)
+
+ # Ensure output is in valid [-1, 1] range
+ result = torch.clamp(result, -1.0, 1.0)
+
+ # Convert from [batch, height, width, 1] to [batch, 4, height, width]
+ result = result.permute(0, 3, 1, 2) # [batch, 1, height, width]
+ result = result.expand(-1, 4, -1, -1) # [batch, 4, height, width]
+
+ # Final verification of output dimensions
+ if result.shape[2:] != (height, width):
+ pass # Added pass statement to fix linter error
+
+ return result
+
+ @staticmethod
+ def temporal_spectral_noise(p, scale, warp_strength, phase_shift, octaves, frequency_range, time, device, base_seed):
+ """
+ Generate spectral noise with true temporal coherence across frames
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ scale: Scale factor
+ warp_strength: Amount of warping
+ phase_shift: Phase/rotation adjustment
+ octaves: Number of detail levels (1-8)
+ frequency_range: Type of frequency filtering (1-4)
+ time: Animation time parameter (critical for temporal coherence)
+ device: Device to use for computation
+ base_seed: Base random seed value (consistent across frames)
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = p.shape
+
+ # Scale by user parameter
+ p = p * scale
+
+ # Extend 2D coordinates to 3D by adding time as third dimension
+ # This is the key to temporal coherence
+ p_temporal = torch.cat([
+ p,
+ torch.ones_like(p[:, :, :, :1]) * time
+ ], dim=-1) # [batch, height, width, 3] where third dimension is time
+
+ # Apply 3D warp if enabled
+ if warp_strength > 0.0:
+ # Create a smooth 3D flow field for warping
+ # Note: Using 3D noise for warping instead of 2D noise with time offset
+ warp_p = p_temporal * 0.4 # Scale for warp coords
+
+ # Generate 3D noise for each component of the warp
+ # Add seed dimension for 3D simplex noise
+ warp_seed1 = torch.ones_like(warp_p[:, :, :, :1]) * base_seed
+ warp_p_with_seed1 = torch.cat([warp_p, warp_seed1], dim=-1)
+
+ warp_seed2 = torch.ones_like(warp_p[:, :, :, :1]) * (base_seed + 1)
+ warp_p_with_seed2 = torch.cat([warp_p + 5.0, warp_seed2], dim=-1)
+
+ # Get noise for x and y dimensions only
+ warp_noise1 = TemporalCoherentNoiseGenerator.simplex_noise_3d(warp_p_with_seed1)
+ warp_noise2 = TemporalCoherentNoiseGenerator.simplex_noise_3d(warp_p_with_seed2)
+
+ # Create warp vectors - shape [batch, height, width, 1]
+ # Now properly expand to match p_temporal's shape
+ zeros = torch.zeros_like(warp_noise1)
+
+ # Create spatial warp vector with explicit shape control
+ # Make sure dimensions match exactly with p_temporal
+ x_warp = warp_noise1 # [batch, height, width, 1]
+ y_warp = warp_noise2 # [batch, height, width, 1]
+ t_warp = zeros # [batch, height, width, 1] - no warp in time dimension
+
+ # Apply the warp to coordinates, component-wise
+ # Make sure we're doing the addition with properly aligned tensors
+ try:
+ p_temporal_x = p_temporal[:, :, :, 0:1] + x_warp * warp_strength
+ p_temporal_y = p_temporal[:, :, :, 1:2] + y_warp * warp_strength
+ p_temporal_t = p_temporal[:, :, :, 2:3] # Time dimension unchanged
+
+ # Recombine the warped components
+ p_temporal = torch.cat([p_temporal_x, p_temporal_y, p_temporal_t], dim=-1)
+ except RuntimeError as e:
+ # Alternative approach - reshape noise to match p_temporal dimensions
+ # This ensures that height and width dimensions match exactly
+ if x_warp.shape[1:3] != p_temporal.shape[1:3]:
+ # Transpose the height and width dimensions if needed
+ if (x_warp.shape[1] == p_temporal.shape[2] and
+ x_warp.shape[2] == p_temporal.shape[1]):
+ # Need to transpose dimensions 1 and 2
+ x_warp = x_warp.permute(0, 2, 1, 3)
+ y_warp = y_warp.permute(0, 2, 1, 3)
+ t_warp = t_warp.permute(0, 2, 1, 3)
+ else:
+ # Try to resize the noise tensors to match
+ target_height, target_width = p_temporal.shape[1:3]
+ x_warp = torch.nn.functional.interpolate(
+ x_warp.permute(0, 3, 1, 2), # [B, C, H, W]
+ size=(target_height, target_width),
+ mode='bilinear'
+ ).permute(0, 2, 3, 1) # Back to [B, H, W, C]
+
+ y_warp = torch.nn.functional.interpolate(
+ y_warp.permute(0, 3, 1, 2),
+ size=(target_height, target_width),
+ mode='bilinear'
+ ).permute(0, 2, 3, 1)
+
+ t_warp = torch.zeros(batch, target_height, target_width, 1, device=device)
+
+ # Try again with the reshaped tensors
+ p_temporal_x = p_temporal[:, :, :, 0:1] + x_warp * warp_strength
+ p_temporal_y = p_temporal[:, :, :, 1:2] + y_warp * warp_strength
+ p_temporal_t = p_temporal[:, :, :, 2:3] # Time dimension unchanged
+
+ # Recombine the warped components
+ p_temporal = torch.cat([p_temporal_x, p_temporal_y, p_temporal_t], dim=-1)
+
+ # Calculate frequency domain coordinates
+ uv = p_temporal[:, :, :, :2] # Use spatial dimensions for UV mapping
+ freq = (p_temporal[:, :, :, :2] - 0.5) * 2.0
+
+ # Calculate radius and angle in frequency space
+ radius = torch.sqrt(freq[:, :, :, 0]**2 + freq[:, :, :, 1]**2)
+ angle = torch.atan2(freq[:, :, :, 1], freq[:, :, :, 0])
+
+ # Apply phase shift with time component for smooth rotation
+ # This creates smooth transitions between frames
+ angle = angle + (phase_shift + time * 0.1) * math.pi
+
+ # Recalculate frequency coordinates after rotation
+ freq_rotated_x = radius * torch.cos(angle)
+ freq_rotated_y = radius * torch.sin(angle)
+ freq = torch.stack([freq_rotated_x, freq_rotated_y], dim=-1)
+
+ # Initialize filter based on frequency range
+ filter_tensor = torch.ones_like(radius)
+
+ # Radial filtering based on frequency range (with time modulation)
+ if frequency_range == 1: # Temporal low-pass
+ cutoff = 0.25 + 0.05 * torch.sin(torch.tensor(time * 0.2, device=device))
+ filter_tensor = torch.sigmoid((1.0 - radius - cutoff) * 10.0)
+ elif frequency_range == 2: # Temporal band-pass
+ center = 0.5 + 0.1 * torch.sin(torch.tensor(time * 0.3, device=device))
+ width = 0.2 + 0.05 * torch.cos(torch.tensor(time * 0.25, device=device))
+ low, high = center - width/2, center + width/2
+ low_pass = torch.sigmoid((radius - low) * 10.0)
+ high_pass = torch.sigmoid((high - radius) * 10.0)
+ filter_tensor = low_pass * high_pass
+ elif frequency_range == 3: # Temporal high-pass
+ cutoff = 0.6 + 0.05 * torch.sin(torch.tensor(time * 0.15, device=device))
+ filter_tensor = torch.sigmoid((radius - cutoff) * 10.0)
+ elif frequency_range == 4: # Directional filter with temporal rotation
+ # Directional filtering - rotates smoothly over time
+ dir_strength = 0.8
+ num_dir = 4 # Number of directions/lobes
+ angle_mod = (angle + time * 0.2) % (2.0 * math.pi)
+ dir_filter = 0.5 + 0.5 * torch.cos(torch.tensor(float(num_dir), device=device) * angle_mod)
+ filter_tensor = torch.lerp(torch.ones_like(dir_filter), dir_filter, dir_strength)
+
+ # Generate base noise with different frequency components using 3D noise
+ noise = torch.zeros_like(radius).unsqueeze(-1)
+
+ # Determine number of octaves based on user control
+ max_octaves = min(int(octaves), 8)
+
+ # Sum multiple octaves with different frequencies
+ for i in range(max_octaves):
+ # Scale frequency based on octave
+ freq_scale = 2.0 ** i
+
+ # Scale amplitude based on frequency (1/f noise)
+ amp = 1.0 / freq_scale
+
+ # Use 3D simplex noise to get temporally coherent noise
+ # We add a different phase offset per octave
+ octave_p = p_temporal * freq_scale + torch.tensor([0.0, 0.0, i * 1.5], device=device)
+
+ # Get 3D noise value
+ noise_val = TemporalCoherentNoiseGenerator.simplex_noise_3d(
+ torch.cat([octave_p, torch.ones_like(octave_p[:, :, :, :1]) * (base_seed + i)], dim=-1)
+ )
+
+ # Apply frequency-based filtering
+ freq_factor = i / max(max_octaves - 1, 1) # 0-1 range for frequency
+ freq_filter = torch.ones_like(noise_val)
+
+ if frequency_range == 1: # Low-pass
+ freq_filter = 1.0 - freq_factor
+ elif frequency_range == 2: # Band-pass
+ freq_filter = 1.0 - torch.abs(torch.tensor(freq_factor, device=device) - 0.5) * 2.0
+ elif frequency_range == 3: # High-pass
+ freq_filter = freq_factor
+
+ noise = noise + (noise_val * amp * freq_filter)
+
+ # Apply filter and normalize
+ noise = noise * filter_tensor.unsqueeze(-1)
+
+ # Add smooth temporal modulations
+ time_factor = torch.sin(torch.tensor(time * 0.3, device=device))
+ noise = noise * (1.0 + 0.1 * time_factor)
+
+ # Normalize to [-1, 1] range
+ noise = torch.clamp(noise * 1.5, -1.0, 1.0)
+
+ return noise
+
+ @staticmethod
+ def simplex_noise_3d(coords):
+ """
+ 3D simplex noise implementation in PyTorch
+ This is the key to temporal coherence as it treats time as a full dimension
+
+ Args:
+ coords: Coordinate tensor [batch, height, width, 4]
+ where dims are [x, y, time, seed]
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = coords.shape
+ device = coords.device
+
+ # Extract x, y, z (time), and seed components
+ x, y, z, seed = coords[:, :, :, 0], coords[:, :, :, 1], coords[:, :, :, 2], coords[:, :, :, 3]
+
+ # 3D Simplex noise constants
+ F3 = 1.0 / 3.0
+ G3 = 1.0 / 6.0
+
+ # Skew input space to determine which simplex cell contains point
+ s = (x + y + z) * F3
+ i = torch.floor(x + s)
+ j = torch.floor(y + s)
+ k = torch.floor(z + s)
+
+ t = (i + j + k) * G3
+ X0 = i - t
+ Y0 = j - t
+ Z0 = k - t
+
+ # Unskewed distances from cell origin
+ x0 = x - X0
+ y0 = y - Y0
+ z0 = z - Z0
+
+ # Determine which simplex we are in
+ # Offsets for second corner of simplex
+ i1 = torch.zeros_like(x0)
+ j1 = torch.zeros_like(y0)
+ k1 = torch.zeros_like(z0)
+
+ # Offsets for third corner of simplex
+ i2 = torch.zeros_like(x0)
+ j2 = torch.zeros_like(y0)
+ k2 = torch.zeros_like(z0)
+
+ # This logic is hard to vectorize, but possible
+ # Using temporary masks to determine the simplex case
+ x_ge_y = (x0 >= y0).float()
+ y_ge_z = (y0 >= z0).float()
+ x_ge_z = (x0 >= z0).float()
+
+ # i1
+ i1 = x_ge_y * x_ge_z
+ # j1
+ j1 = (1 - x_ge_y) * y_ge_z
+ # k1
+ k1 = (1 - x_ge_z) * (1 - y_ge_z)
+
+ # i2
+ i2 = x_ge_y + (1 - x_ge_y) * x_ge_z
+ # j2
+ j2 = x_ge_y * (1 - x_ge_z) + (1 - x_ge_y)
+ # k2
+ k2 = (1 - x_ge_z) + x_ge_z * (1 - x_ge_y)
+
+ # Calculate noise contributions from each corner
+ # 3D noise contributions from simplex corners
+ noise = torch.zeros_like(x0)
+
+ # Corner 1 - origin of simplex
+ t0 = 0.6 - x0*x0 - y0*y0 - z0*z0
+ mask0 = (t0 >= 0).float()
+ t0 = t0 * t0
+ n0 = mask0 * t0 * t0 * TemporalCoherentNoiseGenerator.grad3d(i, j, k, x0, y0, z0, seed)
+
+ # Corner 2
+ x1 = x0 - i1 + G3
+ y1 = y0 - j1 + G3
+ z1 = z0 - k1 + G3
+ t1 = 0.6 - x1*x1 - y1*y1 - z1*z1
+ mask1 = (t1 >= 0).float()
+ t1 = t1 * t1
+ n1 = mask1 * t1 * t1 * TemporalCoherentNoiseGenerator.grad3d(i + i1, j + j1, k + k1, x1, y1, z1, seed)
+
+ # Corner 3
+ x2 = x0 - i2 + 2.0 * G3
+ y2 = y0 - j2 + 2.0 * G3
+ z2 = z0 - k2 + 2.0 * G3
+ t2 = 0.6 - x2*x2 - y2*y2 - z2*z2
+ mask2 = (t2 >= 0).float()
+ t2 = t2 * t2
+ n2 = mask2 * t2 * t2 * TemporalCoherentNoiseGenerator.grad3d(i + i2, j + j2, k + k2, x2, y2, z2, seed)
+
+ # Corner 4 (last corner of simplex)
+ x3 = x0 - 1.0 + 3.0 * G3
+ y3 = y0 - 1.0 + 3.0 * G3
+ z3 = z0 - 1.0 + 3.0 * G3
+ t3 = 0.6 - x3*x3 - y3*y3 - z3*z3
+ mask3 = (t3 >= 0).float()
+ t3 = t3 * t3
+ n3 = mask3 * t3 * t3 * TemporalCoherentNoiseGenerator.grad3d(i + 1, j + 1, k + 1, x3, y3, z3, seed)
+
+ # Sum up noise contributions
+ # Scale to stay within [-1,1]
+ result = (n0 + n1 + n2 + n3) * 32.0
+
+ # Ensure we maintain the input height/width dimensions
+ if result.shape[1:3] != (height, width):
+ # Reshape to match the input dimensions
+ result = result.reshape(batch, height, width).unsqueeze(-1)
+
+ return result.unsqueeze(-1) if len(result.shape) == 3 else result
+
+ @staticmethod
+ def grad3d(ix, iy, iz, x, y, z, seed):
+ """
+ 3D gradient function for simplex noise
+ Computes the dot product of a random gradient vector and a distance vector
+
+ Args:
+ ix, iy, iz: Grid cell coordinates
+ x, y, z: Position relative to grid cell
+ seed: Seed value
+
+ Returns:
+ Dot product of gradient and distance vectors
+ """
+ # Simple hash for deterministic, repeatable patterns
+ # Convert to integers before applying bitwise operations
+ # Need to use mod instead of bitwise & for floats
+ ix_int = torch.floor(ix).to(torch.int32)
+ iy_int = torch.floor(iy).to(torch.int32)
+ iz_int = torch.floor(iz).to(torch.int32)
+ seed_int = torch.floor(seed).to(torch.int32)
+
+ # Compute hash (now with integer tensors)
+ h = (ix_int * 1619 + iy_int * 31337 + iz_int * 6971 + seed_int * 1013) & 15
+
+ # Convert back to original dtype for the rest of the calculations
+ h = h.to(x.dtype)
+
+ # Convert lower 4 bits of hash to 12 gradient directions
+ u = torch.where(h < 8, x, y)
+ v = torch.where(h < 4, y, torch.where((h == 12) | (h == 14), x, z))
+
+ # Convert hash to 12 gradient directions
+ # 1 or -1 based on hash bits - avoiding bitwise operations on floats
+ # The & operation is replaced with modulo division which is safer for floats
+ h_mod_2 = torch.remainder(h, 2)
+ h_mod_4 = torch.remainder(h, 4)
+
+ u_sign = (h_mod_2 == 0).float() * 2.0 - 1.0
+ v_sign = (h_mod_4 < 2).float() * 2.0 - 1.0
+
+ return u_sign * u + v_sign * v
+
+def add_temporal_noise_to_shader_params(shader_params, frame_number, base_seed, fps=24, duration=5):
+ """
+ Prepare shader parameters for temporal coherent noise
+
+ Args:
+ shader_params: Original shader parameters dictionary
+ frame_number: Current frame number
+ base_seed: Base seed for the entire animation
+ fps: Frames per second
+ duration: Animation duration in seconds
+
+ Returns:
+ Modified shader parameters with temporal settings
+ """
+ # Create a copy to avoid modifying the original
+ params = shader_params.copy()
+
+ # Calculate time value based on frame number (0.0 to 1.0 over duration)
+ time = (frame_number / (fps * duration)) % 1.0
+
+ # Store time parameter for the shader
+ params["time"] = time
+
+ # Store the base seed (constant across frames)
+ params["base_seed"] = base_seed
+
+ # Add subtle phase progression for temporal interest
+ # This creates a smooth progression that's continuous
+ base_phase = params.get("shaderPhaseShift", 0.5)
+ frame_phase = base_phase + time * 0.4 # Subtle phase change over time
+ params["shaderPhaseShift"] = frame_phase
+
+ return params
+
+def generate_temporal_coherent_noise_tensor(shader_params, height, width, batch_size=1, device="cuda", seed=0):
+ """
+ Generate temporal coherent noise tensor directly using shader_params
+
+ Args:
+ shader_params: Dictionary containing parameters from shader_params.json
+ height: Height of tensor
+ width: Width of tensor
+ batch_size: Number of images in batch
+ device: Device to create tensor on
+ seed: Random seed to ensure consistent noise with same seed
+
+ Returns:
+ Noise tensor with shape [batch_size, 4, height, width]
+ """
+ # Extract temporal coherence parameters if present
+ time = shader_params.get("time", 0.0)
+ base_seed = shader_params.get("base_seed", seed)
+ use_temporal_coherence = shader_params.get("useTemporalCoherence", True) # Default is True for this type
+
+ # Set deterministic seed for this operation
+ torch.manual_seed(base_seed if use_temporal_coherence else seed)
+
+ # Generate temporal coherent noise using the TemporalCoherentNoiseGenerator
+ noise_bhwc = TemporalCoherentNoiseGenerator.get_temporal_noise(
+ batch_size=batch_size,
+ height=height,
+ width=width,
+ shader_params=shader_params,
+ device=device,
+ base_seed=base_seed if use_temporal_coherence else seed
+ )
+
+ # Reset random seed state to not affect other operations
+ torch.manual_seed(torch.seed())
+
+ # Ensure we have the right format - should already be [batch, 4, height, width]
+ if noise_bhwc.shape[1] == 1:
+ noise_bhwc = noise_bhwc.expand(-1, 4, -1, -1)
+
+ # Normalize the noise to match typical noise distribution
+ noise_bhwc = (noise_bhwc - noise_bhwc.mean()) / (noise_bhwc.std() + 1e-8)
+
+ return noise_bhwc
+
+def integrate_temporal_coherent_noise():
+ """
+ Function to integrate temporal coherent noise into shader_to_tensor.py
+
+ This adds the temporal_coherent_noise method to the ShaderToTensor class
+ """
+ from ..shader_to_tensor import ShaderToTensor
+
+ def temporal_coherent_noise(cls, batch_size, height, width, shader_params, time, device, base_seed):
+ """
+ Generate temporally coherent noise for animation frames
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Dictionary containing parameters
+ time: Animation time
+ device: Device to create tensor on
+ base_seed: Base seed value (consistent across animation)
+
+ Returns:
+ Noise tensor [batch_size, 4, height, width]
+ """
+ # Update shader parameters with time value
+ updated_params = shader_params.copy()
+ updated_params["time"] = time
+
+ # Call the temporal coherent noise generator
+ noise = TemporalCoherentNoiseGenerator.get_temporal_noise(
+ batch_size, height, width, updated_params, device, base_seed
+ )
+
+ return noise
+
+ # Add the method to ShaderToTensor class
+ setattr(ShaderToTensor, 'temporal_coherent_noise', classmethod(temporal_coherent_noise))
+
+ # Register in shader type handlers
+ if hasattr(ShaderToTensor, 'shader_handlers') and isinstance(ShaderToTensor.shader_handlers, dict):
+ ShaderToTensor.shader_handlers['temporal_coherent'] = ShaderToTensor.temporal_coherent_noise
+
+ return True
\ No newline at end of file
diff --git a/shaders/tensor_field.py b/shaders/tensor_field.py
new file mode 100644
index 0000000..9641bc4
--- /dev/null
+++ b/shaders/tensor_field.py
@@ -0,0 +1,1478 @@
+import torch
+import math
+# Import ShaderParamsReader for apply_shape_mask functionality
+from ..shader_params_reader import ShaderParamsReader
+
+class TensorFieldGenerator:
+ """
+ PyTorch implementation of tensor field animation that closely matches
+ the WebGL tensor field shader from shader_renderer.js
+
+ This class generates tensor field patterns that can be used to influence the
+ sampling process in image generation.
+ Used to influence the sampling process in image generation.
+
+ Now enhanced with temporal coherence for smooth animations.
+ """
+
+ @staticmethod
+ def get_tensor_field(batch_size, height, width, shader_params, device="cuda", seed=0):
+ """
+ Generate tensor field noise tensor that matches the WebGL implementation
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Dictionary containing parameters from shader_params.json
+ device: Device to create tensor on
+ seed: Random seed value to ensure different patterns with different seeds
+
+ Returns:
+ Tensor with shape [batch_size, target_channels, height, width]
+ """
+ # Get debugger instance
+ # debugger = get_debugger()
+
+ # Extract parameters from shader_params - check both original and converted names
+ scale = shader_params.get("shaderScale", shader_params.get("scale", 1.0))
+ warp_strength = shader_params.get("shaderWarpStrength", shader_params.get("warp_strength", 0.5))
+ phase_shift = shader_params.get("shaderPhaseShift", shader_params.get("phase_shift", 0.5))
+ octaves = shader_params.get("shaderOctaves", shader_params.get("octaves", 1))
+ time = shader_params.get("time", 0.0)
+
+ base_seed = shader_params.get("base_seed", seed)
+ use_temporal_coherence = shader_params.get("temporal_coherence", shader_params.get("useTemporalCoherence", False))
+
+ shape_type = shader_params.get("shaderShapeType", shader_params.get("shape_type", "none"))
+ shape_mask_strength = shader_params.get("shaderShapeStrength", shader_params.get("shapemaskstrength", 1.0))
+
+ color_scheme = shader_params.get("colorScheme", shader_params.get("color_scheme", "none"))
+ color_intensity = shader_params.get("shaderColorIntensity", shader_params.get("color_intensity", 0.8))
+
+ target_channels = shader_params.get("target_channels", 4)
+ model_class = shader_params.get("model_class", "")
+ inner_model_class = shader_params.get("inner_model_class", "")
+
+ if inner_model_class == "CosmosVideo" or model_class == "CosmosVideo":
+ target_channels = 16
+ shader_params["target_channels"] = 16
+ elif inner_model_class == "ACEStep":
+ target_channels = 8
+ shader_params["target_channels"] = 8
+ elif inner_model_class == "WAN21" or model_class == "WAN21":
+ target_channels = 16
+ shader_params["target_channels"] = 16
+
+ # Only track tensor shapes, not dictionaries
+ if isinstance(shader_params, torch.Tensor):
+ #debugger.track_tensor_shape_history(shader_params, "shader_params_input", "initial_parameters")
+ pass
+
+ y, x = torch.meshgrid(
+ torch.linspace(0, 1, height, device=device),
+ torch.linspace(0, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ #debugger.track_tensor_shape_history(p, "p_coord_grid", "coordinate_grid_creation")
+ pass
+
+ all_generated_channels_list = []
+ loop_seed_master = base_seed if use_temporal_coherence else seed
+
+ # Define color channel specific variations (g_time_offset, b_time_offset etc.)
+ # This block is from the original color path, made available for the loop.
+ g_time_offset, b_time_offset = 0.0, 0.0
+ g_vis_type, b_vis_type = int(octaves % 4), int(octaves % 4)
+ g_scale, b_scale = scale, scale
+ g_warp, b_warp = warp_strength, warp_strength
+
+ if color_scheme != "none" and color_intensity > 0:
+ intensity_factor = 0.5 + color_intensity * 0.5
+ if color_scheme.lower() == "rainbow": # Removed "magma"
+ g_time_offset = 0.33 * intensity_factor
+ b_time_offset = 0.66 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 1) % 4 if intensity_factor > 0.7 else int(octaves % 4)
+ b_vis_type = (int(octaves % 4) + 2) % 4 if intensity_factor > 0.7 else int(octaves % 4)
+ g_scale = scale * (1.0 + 0.1 * intensity_factor)
+ b_scale = scale * (1.0 - 0.1 * intensity_factor)
+ g_warp = warp_strength * (1.0 - 0.2 * intensity_factor)
+ b_warp = warp_strength * (1.0 + 0.2 * intensity_factor)
+ elif color_scheme.lower() == "magma":
+ g_time_offset = 0.40 * intensity_factor
+ b_time_offset = 0.70 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 2) % 4 # Different viz type for G
+ b_vis_type = (int(octaves % 4) + 0) % 4 # B can use same as R, but other params differ
+ g_scale = scale * (1.0 + 0.12 * intensity_factor)
+ b_scale = scale * (1.0 - 0.18 * intensity_factor)
+ g_warp = warp_strength * (1.0 - 0.12 * intensity_factor)
+ b_warp = warp_strength * (1.0 + 0.18 * intensity_factor)
+ elif color_scheme.lower() == "inferno":
+ g_time_offset = 0.15 * intensity_factor # Smaller time offset for G
+ b_time_offset = 0.35 * intensity_factor # Smaller time offset for B
+ g_vis_type = (int(octaves % 4) + 3) % 4 # Different viz type for G
+ b_vis_type = (int(octaves % 4) + 1) % 4 # Different viz type for B, distinct from G
+ g_scale = scale * (1.0 - 0.05 * intensity_factor) # Slight scale decrease for G
+ b_scale = scale * (1.0 + 0.05 * intensity_factor) # Slight scale increase for B
+ g_warp = warp_strength * (1.0 + 0.05 * intensity_factor) # Slight warp increase for G
+ b_warp = warp_strength * (1.0 - 0.1 * intensity_factor) # Slight warp decrease for B
+ elif color_scheme.lower() == "turbo":
+ g_time_offset = 0.10 * intensity_factor
+ b_time_offset = 0.20 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 0) % 4 # G can use same viz as R initially
+ b_vis_type = (int(octaves % 4) + 2) % 4 # B uses a shifted viz type
+ g_scale = scale * (1.0 + 0.20 * intensity_factor) # More significant scale change for G
+ b_scale = scale * (1.0 - 0.20 * intensity_factor) # More significant scale change for B
+ g_warp = warp_strength * (1.0 - 0.08 * intensity_factor)
+ b_warp = warp_strength * (1.0 + 0.22 * intensity_factor)
+ elif color_scheme.lower() == "plasma":
+ g_time_offset = 0.25 * intensity_factor
+ b_time_offset = 0.55 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 2) % 4 # Shift visualization type for G
+ b_vis_type = int(octaves % 4) # B can use same viz type as R but with different params
+ g_scale = scale * (1.0 - 0.15 * intensity_factor) # Slightly different scale for G
+ b_scale = scale * (1.0 + 0.15 * intensity_factor) # Slightly different scale for B
+ g_warp = warp_strength * (1.0 + 0.1 * intensity_factor) # Different warp for G
+ b_warp = warp_strength * (1.0 - 0.25 * intensity_factor) # Different warp for B
+ elif color_scheme.lower() == "jet":
+ g_time_offset = 0.30 * intensity_factor
+ b_time_offset = 0.60 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 1) % 4
+ b_vis_type = (int(octaves % 4) + 3) % 4
+ g_scale = scale * (1.0 - 0.07 * intensity_factor)
+ b_scale = scale * (1.0 + 0.11 * intensity_factor)
+ g_warp = warp_strength * (1.0 + 0.13 * intensity_factor)
+ b_warp = warp_strength * (1.0 - 0.09 * intensity_factor)
+ elif color_scheme.lower() == "viridis":
+ g_time_offset = 0.20 * intensity_factor
+ b_time_offset = 0.40 * intensity_factor # Corrected from 0.50 from previous bad edit
+ g_vis_type = (int(octaves % 4) + 1) % 4
+ b_vis_type = (int(octaves % 4) + 3) % 4 # Using +3 for a different variation pattern
+ g_scale = scale * (1.0 + 0.08 * intensity_factor) # Corrected from -0.07
+ b_scale = scale * (1.0 - 0.08 * intensity_factor) # Corrected from +0.11
+ g_warp = warp_strength * (1.0 - 0.15 * intensity_factor) # Corrected from +0.13
+ b_warp = warp_strength * (1.0 + 0.15 * intensity_factor) # Corrected from -0.09
+ elif color_scheme.lower() == "blue_red":
+ # R channel (i=0) will use base parameters by default.
+ # G channel (i=1) - Aim for raw value of -1.0.
+ # Parameters set for minimal G, will be overwritten later if color_intensity > 0.
+ g_time_offset = 0.0 # No specific time shift for G from R
+ g_vis_type = 0 # Eigenvalue magnitude
+ g_scale = scale * 0.01 # Very small scale for G
+ g_warp = 0.0 # No warp for G
+
+ # B channel (i=2) - Aim for inverted R.
+ r_viz_type = int(octaves % 4)
+ if r_viz_type == 2: # If R's viz_type is hyperstreamlines
+ b_time_offset = math.pi
+ b_vis_type = 2
+ else:
+ # Fallback: make B different from R
+ b_time_offset = 0.5 * intensity_factor
+ b_vis_type = (r_viz_type + 1) % 4
+ b_scale = scale # Same scale as R
+ b_warp = warp_strength # Same warp as R
+ elif color_scheme.lower() == "hot":
+ g_time_offset = 0.1 * intensity_factor
+ b_time_offset = 0.7 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 1) % 4
+ b_vis_type = (int(octaves % 4) + 3) % 4
+ g_scale = scale * (1.0 + 0.15 * intensity_factor)
+ b_scale = scale * (1.0 - 0.15 * intensity_factor)
+ g_warp = warp_strength * (1.0 - 0.1 * intensity_factor)
+ b_warp = warp_strength * (1.0 + 0.1 * intensity_factor)
+ elif color_scheme.lower() == "cool":
+ g_time_offset = 0.6 * intensity_factor
+ b_time_offset = 0.2 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 0) % 4
+ b_vis_type = (int(octaves % 4) + 2) % 4
+ g_scale = scale * (1.0 - 0.1 * intensity_factor)
+ b_scale = scale * (1.0 + 0.1 * intensity_factor)
+ g_warp = warp_strength * (1.0 + 0.2 * intensity_factor)
+ b_warp = warp_strength * (1.0 - 0.2 * intensity_factor)
+ elif color_scheme.lower() == "parula":
+ g_time_offset = 0.22 * intensity_factor
+ b_time_offset = 0.48 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 2) % 4
+ b_vis_type = (int(octaves % 4) + 0) % 4
+ g_scale = scale * (1.0 + 0.03 * intensity_factor)
+ b_scale = scale * (1.0 - 0.06 * intensity_factor)
+ g_warp = warp_strength * (1.0 - 0.04 * intensity_factor)
+ b_warp = warp_strength * (1.0 + 0.07 * intensity_factor)
+ elif color_scheme.lower() == "hsv": # HSV in curl_noise uses hue from angle, value from magnitude
+ g_time_offset = 0.5 * intensity_factor # For G, let's try a phase shift
+ b_time_offset = 0.0 # For B, let's try no major time shift from R for magnitude-like effect
+ g_vis_type = (int(octaves % 4) + 1) % 4 # G will have a different pattern
+ b_vis_type = 0 # B will try to represent magnitude (eigenvalue viz type)
+ g_scale = scale * (1.0 + 0.05 * intensity_factor)
+ b_scale = scale * 0.8 # B scale slightly reduced
+ g_warp = warp_strength
+ b_warp = warp_strength * 0.5 # B warp reduced
+ elif color_scheme.lower() == "autumn":
+ g_time_offset = 0.18 * intensity_factor
+ b_time_offset = 0.38 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 3) % 4
+ b_vis_type = (int(octaves % 4) + 1) % 4
+ g_scale = scale * (1.0 - 0.02 * intensity_factor)
+ b_scale = scale * (1.0 + 0.04 * intensity_factor)
+ g_warp = warp_strength * (1.0 + 0.06 * intensity_factor)
+ b_warp = warp_strength * (1.0 - 0.08 * intensity_factor)
+ elif color_scheme.lower() == "winter":
+ g_time_offset = 0.65 * intensity_factor
+ b_time_offset = 0.25 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 0) % 4
+ b_vis_type = (int(octaves % 4) + 2) % 4
+ g_scale = scale * (1.0 - 0.12 * intensity_factor)
+ b_scale = scale * (1.0 + 0.08 * intensity_factor)
+ g_warp = warp_strength * (1.0 + 0.18 * intensity_factor)
+ b_warp = warp_strength * (1.0 - 0.22 * intensity_factor)
+ elif color_scheme.lower() == "spring":
+ g_time_offset = 0.35 * intensity_factor
+ b_time_offset = 0.75 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 1) % 4
+ b_vis_type = (int(octaves % 4) + 3) % 4
+ g_scale = scale * (1.0 + 0.10 * intensity_factor)
+ b_scale = scale * (1.0 - 0.10 * intensity_factor)
+ g_warp = warp_strength * (1.0 - 0.15 * intensity_factor)
+ b_warp = warp_strength * (1.0 + 0.15 * intensity_factor)
+ elif color_scheme.lower() == "summer":
+ g_time_offset = 0.45 * intensity_factor
+ b_time_offset = 0.12 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 2) % 4
+ b_vis_type = (int(octaves % 4) + 0) % 4
+ g_scale = scale * (1.0 + 0.07 * intensity_factor)
+ b_scale = scale * (1.0 - 0.13 * intensity_factor)
+ g_warp = warp_strength * (1.0 + 0.03 * intensity_factor)
+ b_warp = warp_strength * (1.0 - 0.07 * intensity_factor)
+ elif color_scheme.lower() == "copper":
+ g_time_offset = 0.05 * intensity_factor
+ b_time_offset = 0.10 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 0) % 4
+ b_vis_type = (int(octaves % 4) + 0) % 4
+ g_scale = scale * (1.0 - 0.20 * intensity_factor) # G less prominent
+ b_scale = scale * (1.0 - 0.30 * intensity_factor) # B even less prominent
+ g_warp = warp_strength * 0.7
+ b_warp = warp_strength * 0.5
+ elif color_scheme.lower() == "pink":
+ g_time_offset = 0.55 * intensity_factor
+ b_time_offset = 0.80 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 1) % 4
+ b_vis_type = (int(octaves % 4) + 2) % 4
+ g_scale = scale * (1.0 + 0.02 * intensity_factor)
+ b_scale = scale * (1.0 + 0.06 * intensity_factor) # B slightly more prominent scale
+ g_warp = warp_strength * (1.0 - 0.03 * intensity_factor)
+ b_warp = warp_strength * (1.0 + 0.03 * intensity_factor)
+ elif color_scheme.lower() == "bone":
+ g_time_offset = 0.30 * intensity_factor
+ b_time_offset = 0.30 * intensity_factor # G and B similar time shifts
+ g_vis_type = (int(octaves % 4) + 0) % 4
+ b_vis_type = (int(octaves % 4) + 0) % 4 # G and B similar viz
+ g_scale = scale * 0.9
+ b_scale = scale * 0.8
+ g_warp = warp_strength * 0.9
+ b_warp = warp_strength * 0.8
+ elif color_scheme.lower() == "ocean":
+ g_time_offset = 0.25 * intensity_factor
+ b_time_offset = 0.70 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 2) % 4
+ b_vis_type = (int(octaves % 4) + 0) % 4
+ g_scale = scale * (1.0 - 0.05 * intensity_factor)
+ b_scale = scale * (1.0 + 0.15 * intensity_factor)
+ g_warp = warp_strength * (1.0 + 0.10 * intensity_factor)
+ b_warp = warp_strength * (1.0 - 0.10 * intensity_factor)
+ elif color_scheme.lower() == "terrain":
+ g_time_offset = 0.10 * intensity_factor
+ b_time_offset = 0.50 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 3) % 4
+ b_vis_type = (int(octaves % 4) + 1) % 4
+ g_scale = scale * (1.0 + 0.18 * intensity_factor)
+ b_scale = scale * (1.0 - 0.04 * intensity_factor)
+ g_warp = warp_strength * (1.0 - 0.20 * intensity_factor)
+ b_warp = warp_strength * (1.0 + 0.05 * intensity_factor)
+ elif color_scheme.lower() == "neon":
+ g_time_offset = 0.60 * intensity_factor
+ b_time_offset = 0.90 * intensity_factor # Large shift for B
+ g_vis_type = (int(octaves % 4) + 1) % 4
+ b_vis_type = (int(octaves % 4) + 2) % 4
+ g_scale = scale # G same scale as R
+ b_scale = scale # B same scale as R
+ g_warp = warp_strength * (1.0 + 0.25 * intensity_factor) # Stronger G warp
+ b_warp = warp_strength * (1.0 - 0.25 * intensity_factor) # Inverse B warp
+ elif color_scheme.lower() == "fire":
+ g_time_offset = 0.08 * intensity_factor
+ b_time_offset = 0.16 * intensity_factor # B also low offset
+ g_vis_type = (int(octaves % 4) + 0) % 4
+ b_vis_type = (int(octaves % 4) + 0) % 4 # G and B similar to R viz
+ g_scale = scale * (1.0 + 0.25 * intensity_factor) # G stronger scale
+ b_scale = scale * (1.0 - 0.25 * intensity_factor) # B weaker scale
+ g_warp = warp_strength * (1.0 + 0.10 * intensity_factor)
+ b_warp = warp_strength * 0.3 # B very low warp
+ elif color_scheme.lower() in ["complementary"]: # "hot", "cool" removed
+ g_time_offset = 0.5 * intensity_factor
+ b_time_offset = 1.0 * intensity_factor
+ g_vis_type = (int(octaves % 4) + 2) % 4 if intensity_factor > 0.6 else int(octaves % 4)
+ b_vis_type = (int(octaves % 4) + 1) % 4 if intensity_factor > 0.6 else int(octaves % 4)
+ else: # Default variations for other color schemes
+ g_time_offset = 0.2 * intensity_factor
+ b_time_offset = 0.4 * intensity_factor
+ g_vis_type = int(octaves % 4)
+ b_vis_type = int(octaves % 4)
+ g_scale = scale * (1.0 + 0.05 * intensity_factor)
+ b_scale = scale * (1.0 - 0.05 * intensity_factor)
+ g_warp = warp_strength * (1.0 - 0.1 * intensity_factor)
+ b_warp = warp_strength * (1.0 + 0.1 * intensity_factor)
+
+ for i in range(target_channels):
+ current_channel_gen_index = i
+
+ # Initialize current parameters to base/default for this channel
+ current_p = p.clone() # Start with original coordinates
+ current_time = time
+ current_viz_type = int(octaves % 4)
+ current_scale = scale
+ current_warp = warp_strength
+ # Base seed for this channel, will be adjusted for R,G,B if color active
+ current_seed = loop_seed_master + 700 + (current_channel_gen_index * 130)
+
+ torch.manual_seed(current_seed)
+
+ is_rgb_color_channel = color_scheme != "none" and color_intensity > 0 and i < 3
+
+ if is_rgb_color_channel:
+ if i == 0: # Red channel
+ current_seed = base_seed if use_temporal_coherence else seed # Use main seed
+ # Parameters are already set to base (scale, warp_strength, time, octaves%4)
+ elif i == 1: # Green channel
+ current_time = time + g_time_offset
+ current_viz_type = g_vis_type
+ current_scale = g_scale
+ current_warp = g_warp
+ current_seed = (base_seed + 42) if use_temporal_coherence else (seed + 42)
+ elif i == 2: # Blue channel
+ current_time = time + b_time_offset
+ current_viz_type = b_vis_type
+ current_scale = b_scale
+ current_warp = b_warp
+ current_seed = (base_seed + 123) if use_temporal_coherence else (seed + 123)
+ else: # Monochrome path OR additional channels (i >= 3) in color mode
+ # Perturb parameters for structured variation
+ pert_scale_val = 0.005 + (current_channel_gen_index * 0.001) # Small perturbation
+ current_p += (torch.randn_like(p) * pert_scale_val)
+ current_p = torch.clamp(current_p, 0.0, 1.0)
+
+ current_time = time + (current_channel_gen_index * 0.02) # Small time offset
+ current_viz_type = (int(octaves % 4) + current_channel_gen_index) % 4 # Vary viz type
+
+ current_scale_factor = 1.0 + ((current_channel_gen_index % 5 - 2) * 0.03) # Vary scale slightly
+ current_scale = scale * current_scale_factor
+
+ current_warp_factor = 1.0 + ((current_channel_gen_index % 7 - 3) * 0.03) # Vary warp slightly
+ current_warp = warp_strength * current_warp_factor
+ # current_seed is already set by the loop using current_channel_gen_index
+
+ # Generate the single channel noise
+ single_channel_bhw1 = TensorFieldGenerator.tensor_field(
+ current_p, current_viz_type, current_scale, current_warp,
+ current_time, device, current_seed, use_temporal_coherence
+ )
+
+ # Ensure single_channel_bhw1 is [B, H, W, 1]
+ if len(single_channel_bhw1.shape) == 3: # If tensor_field returned [B,H,W]
+ single_channel_bhw1 = single_channel_bhw1.unsqueeze(-1)
+
+ # Post-process the generated channel
+ current_contrast = 1.0 + phase_shift
+ single_channel_bhw1 *= current_contrast
+ single_channel_bhw1 = torch.clamp(single_channel_bhw1, -1.0, 1.0)
+
+ # Specific overwrite for G channel in "blue_red" scheme
+ if is_rgb_color_channel and i == 1 and color_scheme.lower() == "blue_red" and color_intensity > 0:
+ # Force G channel to be -1.0 (raw) to achieve normalized 0 later
+ single_channel_bhw1 = torch.full_like(single_channel_bhw1, -1.0)
+
+ if shape_type not in ["none", "0"] and shape_mask_strength > 0:
+ # Use original p for a consistent mask across channels, or current_p for varied mask
+ p_normalized_for_mask = p * 2.0 - 1.0
+ # p_normalized_for_mask = current_p * 2.0 - 1.0 # Alternative: varied mask
+
+ shape_mask_for_channel = ShaderParamsReader.apply_shape_mask(p_normalized_for_mask, shape_type)
+
+ # Ensure shape_mask_for_channel is [B,H,W,1] for lerp
+ if len(shape_mask_for_channel.shape) == 3: # Is [B,H,W]
+ shape_mask_for_channel = shape_mask_for_channel.unsqueeze(-1)
+ # Expand batch dim if necessary
+ if shape_mask_for_channel.shape[0] != single_channel_bhw1.shape[0]:
+ shape_mask_for_channel = shape_mask_for_channel.expand(single_channel_bhw1.shape[0], -1, -1, -1)
+
+ #debugger.track_tensor_shape_history(shape_mask_for_channel, f"shape_mask_for_channel_ch{i}", "shape_mask_application")
+ #debugger.track_tensor_shape_history(single_channel_bhw1, f"single_channel_bhw1_ch{i}", "shape_mask_application")
+ single_channel_bhw1 = torch.lerp(single_channel_bhw1, single_channel_bhw1 * shape_mask_for_channel, shape_mask_strength)
+ single_channel_bhw1 = torch.clamp(single_channel_bhw1, -1.0, 1.0)
+
+ single_channel_b1hw = single_channel_bhw1.permute(0, 3, 1, 2)
+ #debugger.track_tensor_shape_history(single_channel_b1hw, f"single_channel_b1hw_final_ch{i}", "individual_channel_permutation")
+ all_generated_channels_list.append(single_channel_b1hw)
+
+ final_result_bchw = torch.cat(all_generated_channels_list, dim=1)
+ #debugger.track_tensor_shape_history(final_result_bchw, "final_result_bchw_pre_debug_report", "all_channels_concatenated")
+ # print(f"TensorField: Generated {final_result_bchw.shape[1]} structured channels directly.") # Removed this line
+
+ # Channel count mismatch warning (like curl_noise.py)
+ if final_result_bchw.shape[1] != target_channels:
+ # print(f"โ ๏ธ Channel mismatch: Generated {final_result_bchw.shape[1]} but need {target_channels}") # Removed this line
+ #debugger.add_warning(
+ # f"Channel count mismatch: got {final_result_bchw.shape[1]} but expected {target_channels}",
+ # category="channel_count"
+ #)
+ # Optionally, pad or trim channels to match target_channels
+ corrected = torch.zeros((batch_size, target_channels, height, width), device=device)
+ min_channels = min(final_result_bchw.shape[1], target_channels)
+ corrected[:, :min_channels] = final_result_bchw[:, :min_channels]
+ if final_result_bchw.shape[1] < target_channels:
+ std = final_result_bchw.std()
+ mean = final_result_bchw.mean()
+ for c in range(min_channels, target_channels):
+ phase = (c / target_channels) * 2 * 3.14159
+ corrected[:, c] = torch.sin(p[..., 0].permute(0, 3, 1, 2) * 5 + phase) * std + mean
+ final_result_bchw = corrected
+ #debugger.track_tensor_shape_history(final_result_bchw, "final_result_bchw_corrected", "channel_correction")
+ # print(f"โ Adjusted tensor field to {target_channels} channels for compatibility") # Removed this line
+
+ # Generate a debug report for this tensor generation process
+ #debugger.visualize_tensor_shapes_report()
+ return final_result_bchw
+
+ @staticmethod
+ def compute_tensor_properties(p, scale, warp_strength, time, device, seed, use_temporal_coherence=False):
+ """
+ Compute tensor field properties (eigenvalues and eigenvectors)
+ Matches the computeTensorProperties function in WebGL shader
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2] in [0, 1] range
+ scale: Scale factor
+ warp_strength: Amount of warping
+ time: Animation time
+ device: Device to use for computation
+ seed: Random seed value
+ use_temporal_coherence: Whether to use temporal coherence for animations
+
+ Returns:
+ Tuple of (lambda1, lambda2, v1, v2) where lambda are eigenvalues and v are eigenvectors
+ """
+ batch, height, width, _ = p.shape
+
+ # Offset based on time
+ offset = torch.tensor([[[[time * 0.05, 0.0]]]], device=device)
+ p1 = p * scale + offset
+
+ # Apply warp to coordinates if warp strength is non-zero
+ if warp_strength > 0.0:
+ # Generate warp field based on noise
+ if use_temporal_coherence:
+ # Use 3D noise for temporal coherence
+ warp_noise1 = TensorFieldGenerator.simplex_noise_3d(
+ p1 * 0.3,
+ seed,
+ time_offset=time * 0.2
+ )
+ warp_noise2 = TensorFieldGenerator.simplex_noise_3d(
+ p1 * 0.3,
+ seed + 1,
+ time_offset=time * 0.2 + 3.33
+ )
+ else:
+ # Original implementation
+ warp_noise1 = TensorFieldGenerator.simplex_noise(p1 * 0.3 + torch.tensor([[[[0.0, 1.0]]]], device=device), seed)
+ warp_noise2 = TensorFieldGenerator.simplex_noise(p1 * 0.3 + torch.tensor([[[[1.0, 0.0]]]], device=device), seed + 1)
+
+ # Apply warp to coordinates
+ p1 = p1 + torch.cat([warp_noise1, warp_noise2], dim=-1) * warp_strength
+
+ # Use noise derivatives to generate tensor field
+ eps = 0.01
+
+ # Compute approximate derivatives of noise field
+ if use_temporal_coherence:
+ # Use 3D noise with time for smoother transitions
+ n00 = TensorFieldGenerator.simplex_noise_3d(p1, seed + 2, time_offset=time * 0.1)
+ n10 = TensorFieldGenerator.simplex_noise_3d(p1 + torch.tensor([[[[eps, 0.0]]]], device=device), seed + 2, time_offset=time * 0.1)
+ n01 = TensorFieldGenerator.simplex_noise_3d(p1 + torch.tensor([[[[0.0, eps]]]], device=device), seed + 2, time_offset=time * 0.1)
+ n11 = TensorFieldGenerator.simplex_noise_3d(p1 + torch.tensor([[[[eps, eps]]]], device=device), seed + 2, time_offset=time * 0.1)
+ n_minus_x = TensorFieldGenerator.simplex_noise_3d(p1 - torch.tensor([[[[eps, 0.0]]]], device=device), seed + 2, time_offset=time * 0.1)
+ n_minus_y = TensorFieldGenerator.simplex_noise_3d(p1 - torch.tensor([[[[0.0, eps]]]], device=device), seed + 2, time_offset=time * 0.1)
+ else:
+ # Original implementation
+ n00 = TensorFieldGenerator.simplex_noise(p1, seed + 2)
+ n10 = TensorFieldGenerator.simplex_noise(p1 + torch.tensor([[[[eps, 0.0]]]], device=device), seed + 2)
+ n01 = TensorFieldGenerator.simplex_noise(p1 + torch.tensor([[[[0.0, eps]]]], device=device), seed + 2)
+ n11 = TensorFieldGenerator.simplex_noise(p1 + torch.tensor([[[[eps, eps]]]], device=device), seed + 2)
+ n_minus_x = TensorFieldGenerator.simplex_noise(p1 - torch.tensor([[[[eps, 0.0]]]], device=device), seed + 2)
+ n_minus_y = TensorFieldGenerator.simplex_noise(p1 - torch.tensor([[[[0.0, eps]]]], device=device), seed + 2)
+
+ # Calculate derivatives (gradient components)
+ dx = (n10 - n00) / eps
+ dy = (n01 - n00) / eps
+
+ # Second order derivatives for tensor components
+ dxx = (n10 - 2.0 * n00 + n_minus_x) / (eps * eps)
+ dyy = (n01 - 2.0 * n00 + n_minus_y) / (eps * eps)
+ dxy = (n11 - n10 - n01 + n00) / (eps * eps)
+
+ # Construct tensor matrix components
+ T00 = dxx
+ T01 = dxy
+ T10 = dxy
+ T11 = dyy
+
+ # Calculate eigenvalues
+ trace = T00 + T11
+ det = T00 * T11 - T01 * T10
+ # Use safe sqrt to avoid NaN
+ discriminant = torch.sqrt(torch.clamp(trace * trace - 4.0 * det, min=1e-8))
+
+ # Two eigenvalues
+ lambda1 = (trace + discriminant) * 0.5
+ lambda2 = (trace - discriminant) * 0.5
+
+ # Initialize eigenvectors
+ v1 = torch.zeros_like(p)
+ v2 = torch.zeros_like(p)
+
+ # Expand mask dimensions for proper broadcasting
+ mask_T01 = (torch.abs(T01) > 0.0001).expand(-1, -1, -1, 2)
+ mask_T10 = (torch.abs(T10) > 0.0001).expand(-1, -1, -1, 2)
+
+ # Calculate first eigenvector - handling each case separately
+ # Where T01 is significant, use (T01, lambda1 - T00) direction
+ dir1_T01 = torch.cat([T01, lambda1 - T00], dim=-1)
+ norm1_T01 = torch.norm(dir1_T01, dim=-1, keepdim=True).expand(-1, -1, -1, 2)
+ norm1_T01 = torch.where(norm1_T01 > 1e-8, norm1_T01, torch.ones_like(norm1_T01))
+ v1_T01 = dir1_T01 / norm1_T01
+
+ # Where T10 is significant (and T01 is not), use (lambda1 - T11, T10) direction
+ dir1_T10 = torch.cat([lambda1 - T11, T10], dim=-1)
+ norm1_T10 = torch.norm(dir1_T10, dim=-1, keepdim=True).expand(-1, -1, -1, 2)
+ norm1_T10 = torch.where(norm1_T10 > 1e-8, norm1_T10, torch.ones_like(norm1_T10))
+ v1_T10 = dir1_T10 / norm1_T10
+
+ # Apply the result based on the masks using torch.where instead of indexing
+ mask_T10_not_T01 = mask_T10 & (~mask_T01)
+ v1 = torch.where(mask_T01, v1_T01, v1)
+ v1 = torch.where(mask_T10_not_T01, v1_T10, v1)
+
+ # Diagonal case - where neither T01 nor T10 is significant
+ mask_diag = ~(mask_T01 | mask_T10)
+ v1 = torch.where(mask_diag, torch.tensor([1.0, 0.0], device=device), v1)
+
+ # Second eigenvector is perpendicular to first
+ v2[..., 0] = -v1[..., 1]
+ v2[..., 1] = v1[..., 0]
+
+ # Debugger calls for compute_tensor_properties
+ # It's tricky to place these without knowing the structure of the caller,
+ # but we can track the inputs and outputs of this static method.
+ # The debugger instance would ideally be passed in or accessed globally.
+ # For now, we'll assume a global debugger or skip detailed tracking inside staticmethods
+ # unless it's explicitly passed.
+ # If this method is only called by get_tensor_field, debugger is in scope.
+
+ return lambda1, lambda2, v1, v2
+
+ @staticmethod
+ def tensor_field(p, visualization_type, scale, warp_strength, time, device, seed, use_temporal_coherence=False):
+ """
+ Generate tensor field based on visualization type
+ Matches the tensorField function in WebGL shader
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2] in [0, 1] range
+ visualization_type: Type of visualization (0-3)
+ scale: Scale factor
+ warp_strength: Amount of warping
+ time: Animation time
+ device: Device to use for computation
+ seed: Random seed value
+ use_temporal_coherence: Whether to use temporal coherence for animations
+
+ Returns:
+ Tensor field values [batch, height, width, 1]
+ """
+
+ batch, height, width, _ = p.shape
+
+ # Calculate tensor field components
+ lambda1, lambda2, v1, v2 = TensorFieldGenerator.compute_tensor_properties(
+ p, scale, warp_strength, time, device, seed, use_temporal_coherence
+ )
+
+
+ # Different visualization modes based on type
+ result_tensor = None
+ if visualization_type == 0:
+ # Eigenvalue visualization - shows magnitude of deformation
+ max_eig = torch.maximum(torch.abs(lambda1), torch.abs(lambda2))
+ result_tensor = torch.clamp(max_eig, -1.0, 1.0)
+
+ elif visualization_type == 1:
+ # Eigenvector streamlines - shows direction of principal stress
+ st = p
+ line_width = 0.08
+
+ # Calculate distance to streamline along first eigenvector
+ # Use deterministic flow phase calculation with time for temporal coherence
+ if use_temporal_coherence:
+ flow_phase = torch.tensor(time * 0.2, device=device)
+ else:
+ flow_phase = time * 0.2
+
+ t = st[..., 0] * v1[..., 0] + st[..., 1] * v1[..., 1]
+ streamline1 = torch.abs(torch.frac(t * 5.0 + flow_phase) - 0.5) * 2.0
+
+ # Calculate distance to streamline along second eigenvector
+ t = st[..., 0] * v2[..., 0] + st[..., 1] * v2[..., 1]
+ streamline2 = torch.abs(torch.frac(t * 5.0 - flow_phase) - 0.5) * 2.0
+
+ # Combine streamlines
+ pattern = torch.minimum(streamline1, streamline2)
+ smoothed = 1.0 - torch.clamp(pattern / line_width, 0.0, 1.0) * 2.0
+
+ result_tensor = smoothed.unsqueeze(-1)
+
+ elif visualization_type == 2:
+ # Hyperstreamlines - thickness varies with eigenvalue magnitude
+ # Ensure lambda1 and lambda2 have the right shape
+ lambda1_reshaped = lambda1.clone()
+ lambda2_reshaped = lambda2.clone()
+
+ # Make sure lambda1/lambda2 have the right shape for broadcasting
+ if len(lambda1.shape) == 3:
+ lambda1_reshaped = lambda1_reshaped.unsqueeze(-1)
+ if len(lambda2.shape) == 3:
+ lambda2_reshaped = lambda2_reshaped.unsqueeze(-1)
+
+ # Create direction vectors with correct sign
+ lambda_sign = torch.sign(lambda1_reshaped)
+ dir1 = v1 * lambda_sign
+
+ lambda2_sign = torch.sign(lambda2_reshaped)
+ dir2 = v2 * lambda2_sign
+
+ # Weights based on eigenvalue magnitudes - ensure they have compatible dimensions
+ abs_lambda1 = torch.abs(lambda1_reshaped)
+ abs_lambda2 = torch.abs(lambda2_reshaped)
+
+ # Ensure weight1 has the right shape for broadcasting
+ denominator = abs_lambda1 + abs_lambda2 + 0.001
+ weight1 = abs_lambda1 / denominator
+ weight2 = 1.0 - weight1
+
+ # Make sure weights have shape [batch, height, width]
+ if len(weight1.shape) == 4 and weight1.shape[-1] == 1:
+ weight1 = weight1.squeeze(-1)
+ if len(weight2.shape) == 4 and weight2.shape[-1] == 1:
+ weight2 = weight2.squeeze(-1)
+
+ # Calculate t values along each direction with more stable time implementation for coherence
+ if use_temporal_coherence:
+ t_phase = torch.tensor(time, device=device)
+ t1 = torch.cos(5.0 * (p[..., 0] * dir1[..., 0] + p[..., 1] * dir1[..., 1]) + t_phase)
+ t2 = torch.cos(5.0 * (p[..., 0] * dir2[..., 0] + p[..., 1] * dir2[..., 1]) - t_phase)
+ else:
+ t1 = torch.cos(5.0 * (p[..., 0] * dir1[..., 0] + p[..., 1] * dir1[..., 1]) + time)
+ t2 = torch.cos(5.0 * (p[..., 0] * dir2[..., 0] + p[..., 1] * dir2[..., 1]) - time)
+
+ # Combine with weights - ensure dimensions are compatible for element-wise multiplication
+ result = (t1 * weight1 + t2 * weight2) * 0.5
+
+ result_tensor = result.unsqueeze(-1)
+
+ else: # visualization_type == 3
+ # Tensor ellipses
+
+ # Create an elliptical pattern aligned with eigenvectors and scaled by eigenvalues
+ centered = p - torch.floor(p * 4.0 + 0.5) / 4.0 # Create grid
+
+ # Transform point to eigenvector basis
+ x = torch.sum(centered * v1, dim=-1)
+ y = torch.sum(centered * v2, dim=-1)
+
+ # Ensure lambda1 and lambda2 have shapes compatible with x and y
+ lambda1_tensor = lambda1.clone()
+ lambda2_tensor = lambda2.clone()
+
+ # Make sure lambda1 and lambda2 have the right shape for broadcasting with x and y
+ if len(lambda1.shape) == 4 and lambda1.shape[-1] == 1:
+ lambda1_tensor = lambda1_tensor.squeeze(-1)
+ elif len(lambda1.shape) == 3:
+ # This is already the right shape
+ pass
+ else:
+ # Reshape to match batch, height, width
+ lambda1_tensor = lambda1_tensor.view(batch, height, width)
+
+ if len(lambda2.shape) == 4 and lambda2.shape[-1] == 1:
+ lambda2_tensor = lambda2_tensor.squeeze(-1)
+ elif len(lambda2.shape) == 3:
+ # This is already the right shape
+ pass
+ else:
+ # Reshape to match batch, height, width
+ lambda2_tensor = lambda2_tensor.view(batch, height, width)
+
+ # Scale by eigenvalues (normalized to prevent distortion)
+ # Make sure all tensors have compatible shapes
+ abs_lambda1 = torch.abs(lambda1_tensor)
+ abs_lambda2 = torch.abs(lambda2_tensor)
+ max_eig = torch.maximum(abs_lambda1, abs_lambda2) + 0.1
+
+ # Now all tensors should have shape [batch, height, width]
+ scaled_x = x * abs_lambda1 / max_eig
+ scaled_y = y * abs_lambda2 / max_eig
+
+ # Create ellipse
+ ellipse = torch.sqrt(scaled_x**2 + scaled_y**2)
+ radius = 0.05
+
+ # Animate pulsing ellipses with more deterministic calculation for temporal coherence
+ if use_temporal_coherence:
+ radius *= 1.0 + 0.3 * torch.sin(torch.tensor(time * 2.0, device=device))
+ else:
+ radius *= 1.0 + 0.3 * torch.sin(torch.tensor(time * 2.0, device=device))
+
+ # Return elliptical pattern
+ smoothed = 1.0 - torch.clamp((ellipse - radius) / 0.01, 0.0, 1.0) * 2.0
+
+ result_tensor = smoothed.unsqueeze(-1)
+
+ return result_tensor
+
+ @staticmethod
+ def simplex_noise_3d(coords, seed=0, time_offset=0.0):
+ """
+ 3D Simplex noise implementation in PyTorch
+ This is used for temporal coherence, treating time as a third dimension
+
+ Args:
+ coords: Coordinate tensor [batch, height, width, 2]
+ seed: Random seed value
+ time_offset: Time offset to use as the third dimension
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = coords.shape
+ device = coords.device
+
+ # Create 3D coordinates by adding time as the third dimension
+ coords_3d = torch.cat([
+ coords,
+ torch.ones(batch, height, width, 1, device=device) * time_offset
+ ], dim=-1)
+
+ # Set deterministic seed
+ torch.manual_seed(seed)
+
+ # Constants for skewing and unskewing the grid
+ F3 = 1.0/3.0
+ G3 = 1.0/6.0
+
+ # Skew the input space to determine which simplex cell we're in
+ s = (coords_3d[:, :, :, 0] + coords_3d[:, :, :, 1] + coords_3d[:, :, :, 2]) * F3
+ s = s.unsqueeze(-1)
+
+ i = torch.floor(coords_3d[:, :, :, 0] + s[:, :, :, 0])
+ j = torch.floor(coords_3d[:, :, :, 1] + s[:, :, :, 0])
+ k = torch.floor(coords_3d[:, :, :, 2] + s[:, :, :, 0])
+
+ i = i.unsqueeze(-1)
+ j = j.unsqueeze(-1)
+ k = k.unsqueeze(-1)
+
+ # Unskew the cell origin back to (x,y,z) space
+ t = (i + j + k) * G3
+ X0 = i - t
+ Y0 = j - t
+ Z0 = k - t
+
+ # The x,y,z distances from the cell origin
+ x0 = coords_3d[:, :, :, 0:1] - X0
+ y0 = coords_3d[:, :, :, 1:2] - Y0
+ z0 = coords_3d[:, :, :, 2:3] - Z0
+
+ # For the 3D case, the simplex shape is a tetrahedron
+ # Determine which simplex we're in
+ g1 = torch.zeros_like(x0)
+ g2 = torch.zeros_like(x0)
+ g3 = torch.zeros_like(x0)
+
+ # Find out which of the six possible tetrahedra we're in
+ g1 = (x0 >= y0).float() * (x0 >= z0).float()
+ g2 = ((x0 >= y0).float() * (x0 < z0).float() +
+ (x0 < y0).float() * (y0 >= z0).float())
+ g3 = (x0 < y0).float() * (y0 < z0).float()
+
+ c1 = (g1 > 0.0).float()
+ c2 = (g2 > 0.0).float()
+ c3 = (g3 > 0.0).float()
+
+ # Offsets for corners
+ i1 = c1
+ j1 = c2
+ k1 = c3
+
+ i2 = 1.0 - c3
+ j2 = 1.0 - c1
+ k2 = 1.0 - c2
+
+ i3 = torch.ones_like(i1)
+ j3 = torch.ones_like(j1)
+ k3 = torch.ones_like(k1)
+
+ # Work out the hashed gradient indices of the four simplex corners
+ def hash(i, j, k, seed_val=seed):
+ return ((i * 1664525 + j * 1013904223 + k * 60493) % 16384) / 16384.0 + seed_val/10.0
+
+ # Get random gradients at corners
+ def get_gradient(hash_val):
+ # Convert hash to spherical coordinates
+ theta = hash_val * math.pi * 2.0
+ phi = hash_val * 13.0 # Add variation to phi
+
+ # Convert to cartesian coordinates on unit sphere
+ x = torch.cos(theta) * torch.cos(phi)
+ y = torch.sin(theta) * torch.cos(phi)
+ z = torch.sin(phi)
+
+ return torch.cat([x, y, z], dim=-1)
+
+ # Generate gradients at corners
+ h000 = hash(i, j, k)
+ h100 = hash(i+1, j, k)
+ h010 = hash(i, j+1, k)
+ h110 = hash(i+1, j+1, k)
+ h001 = hash(i, j, k+1)
+ h101 = hash(i+1, j, k+1)
+ h011 = hash(i, j+1, k+1)
+ h111 = hash(i+1, j+1, k+1)
+
+ # Get gradient vectors
+ g000 = get_gradient(h000)
+ g100 = get_gradient(h100)
+ g010 = get_gradient(h010)
+ g110 = get_gradient(h110)
+ g001 = get_gradient(h001)
+ g101 = get_gradient(h101)
+ g011 = get_gradient(h011)
+ g111 = get_gradient(h111)
+
+ # Calculate contribution from each corner
+ # Corner 0
+ t0 = 0.6 - x0*x0 - y0*y0 - z0*z0
+ t0 = torch.maximum(torch.zeros_like(t0), t0)
+ n0 = t0 * t0 * t0 * t0 * torch.sum(
+ torch.cat([x0, y0, z0], dim=-1) * g000, dim=-1, keepdim=True
+ )
+
+ # Corner 1
+ x1 = x0 - i1 + G3
+ y1 = y0 - j1 + G3
+ z1 = z0 - k1 + G3
+ t1 = 0.6 - x1*x1 - y1*y1 - z1*z1
+ t1 = torch.maximum(torch.zeros_like(t1), t1)
+ n1 = t1 * t1 * t1 * t1 * (
+ c1 * (g100[:, :, :, 0:1] * x1 + g100[:, :, :, 1:2] * y1 + g100[:, :, :, 2:3] * z1) +
+ c2 * (g010[:, :, :, 0:1] * x1 + g010[:, :, :, 1:2] * y1 + g010[:, :, :, 2:3] * z1) +
+ c3 * (g001[:, :, :, 0:1] * x1 + g001[:, :, :, 1:2] * y1 + g001[:, :, :, 2:3] * z1)
+ )
+
+ # Corner 2
+ x2 = x0 - i2 + 2.0*G3
+ y2 = y0 - j2 + 2.0*G3
+ z2 = z0 - k2 + 2.0*G3
+ t2 = 0.6 - x2*x2 - y2*y2 - z2*z2
+ t2 = torch.maximum(torch.zeros_like(t2), t2)
+ n2 = t2 * t2 * t2 * t2 * (
+ (1.0-c3) * (g110[:, :, :, 0:1] * x2 + g110[:, :, :, 1:2] * y2 + g110[:, :, :, 2:3] * z2) +
+ (1.0-c1) * (g011[:, :, :, 0:1] * x2 + g011[:, :, :, 1:2] * y2 + g011[:, :, :, 2:3] * z2) +
+ (1.0-c2) * (g101[:, :, :, 0:1] * x2 + g101[:, :, :, 1:2] * y2 + g101[:, :, :, 2:3] * z2)
+ )
+
+ # Corner 3
+ x3 = x0 - i3 + 3.0*G3
+ y3 = y0 - j3 + 3.0*G3
+ z3 = z0 - k3 + 3.0*G3
+ t3 = 0.6 - x3*x3 - y3*y3 - z3*z3
+ t3 = torch.maximum(torch.zeros_like(t3), t3)
+ n3 = t3 * t3 * t3 * t3 * torch.sum(
+ torch.cat([x3, y3, z3], dim=-1) * g111, dim=-1, keepdim=True
+ )
+
+ # Sum up and scale the result to be in [-1,1]
+ noise = 32.0 * (n0 + n1 + n2 + n3)
+
+ return noise
+
+ @staticmethod
+ def simplex_noise(coords, seed=0):
+ """
+ Simplex noise implementation in PyTorch
+ This provides noise values similar to snoise in GLSL
+
+ Args:
+ coords: Coordinate tensor [batch, height, width, 2]
+ seed: Random seed value
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ batch, height, width, _ = coords.shape
+ device = coords.device
+
+ # Set deterministic seed
+ torch.manual_seed(seed)
+
+ # Integer and fractional parts
+ i0 = torch.floor(coords)
+ i1 = i0 + 1.0
+ f0 = coords - i0
+
+ # Dot products with random gradients
+ def random_gradient(p, seed_val=seed):
+ # Simple hash function
+ h = p[:, :, :, 0] * 15.0 + p[:, :, :, 1] * 37.0 + seed_val
+ h = torch.sin(h) * 43758.5453
+ angle = h * 2.0 * math.pi
+ return torch.stack([torch.cos(angle), torch.sin(angle)], dim=-1)
+
+ # Get gradients at corners
+ g00 = random_gradient(i0)
+ g10 = random_gradient(torch.cat([i1[:, :, :, 0:1], i0[:, :, :, 1:2]], dim=-1))
+ g01 = random_gradient(torch.cat([i0[:, :, :, 0:1], i1[:, :, :, 1:2]], dim=-1))
+ g11 = random_gradient(i1)
+
+ # Calculate contributions from each corner
+ def contribution(grad, pos, point):
+ # Calculate falloff
+ t = 0.5 - torch.sum((point - pos) ** 2, dim=-1, keepdim=True)
+ t = torch.maximum(t, torch.zeros_like(t))
+ t = t * t * t * t # Quintic interpolation
+
+ # Dot product of gradient and offset
+ offset = point - pos
+ return t * torch.sum(grad * offset, dim=-1, keepdim=True)
+
+ # Get contributions
+ n00 = contribution(g00, i0, coords)
+ n10 = contribution(g10, torch.cat([i1[:, :, :, 0:1], i0[:, :, :, 1:2]], dim=-1), coords)
+ n01 = contribution(g01, torch.cat([i0[:, :, :, 0:1], i1[:, :, :, 1:2]], dim=-1), coords)
+ n11 = contribution(g11, i1, coords)
+
+ # Smoothstep for interpolation
+ t = f0 * f0 * f0 * (f0 * (f0 * 6.0 - 15.0) + 10.0)
+
+ # Bilinear interpolation with smoothstep
+ n_x0 = n00 * (1.0 - t[:, :, :, 0:1]) + n10 * t[:, :, :, 0:1]
+ n_x1 = n01 * (1.0 - t[:, :, :, 0:1]) + n11 * t[:, :, :, 0:1]
+ noise = n_x0 * (1.0 - t[:, :, :, 1:2]) + n_x1 * t[:, :, :, 1:2]
+
+ # Scale to match WebGL shader output range
+ final_noise = 2.8 * noise
+ return final_noise
+
+def generate_tensor_field_tensor(shader_params, height, width, batch_size=1, device="cuda", seed=0, target_channels=None):
+ """
+ Generate tensor field noise tensor directly using shader_params
+
+ Args:
+ shader_params: Dictionary containing parameters from shader_params.json
+ height: Height of tensor
+ width: Width of tensor
+ batch_size: Number of images in batch
+ device: Device to create tensor on
+ seed: Random seed to ensure consistent noise with same seed
+ target_channels: Optional target number of channels. If provided, will be set in shader_params.
+
+ Returns:
+ Noise tensor with shape [batch_size, final_channels, height, width]
+ """
+ # Get debugger instance
+ # debugger = get_debugger()
+
+ # Extract temporal coherence parameters if present
+ time = shader_params.get("time", 0.0)
+ base_seed = shader_params.get("base_seed", seed)
+ use_temporal_coherence = shader_params.get("useTemporalCoherence", shader_params.get("temporal_coherence", False))
+
+ # If target_channels is provided to this function, add/update it in shader_params
+ # so get_tensor_field can use it.
+ # This function itself doesn't take target_channels as a direct arg in this version,
+ # assuming it's already in shader_params if needed.
+ # For consistency with domain_warp, let's add it as an optional parameter.
+ # The edit below will be to the function signature and its body for this.
+
+ # Track coordinate grid tensor for debugging (like curl_noise.py)
+ y, x = torch.meshgrid(
+ torch.linspace(0, 1, height, device=device),
+ torch.linspace(0, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+ # debugger.track_tensor_shape_history(p, "p_coord_grid", "coordinate_grid_creation")
+
+ # Set deterministic seed for this operation
+ torch.manual_seed(base_seed if use_temporal_coherence else seed)
+
+ # Generate tensor field noise using the TensorFieldGenerator
+ noise_bhwc = TensorFieldGenerator.get_tensor_field(
+ batch_size=batch_size,
+ height=height,
+ width=width,
+ shader_params=shader_params, # shader_params now contains target_channels if provided
+ device=device,
+ seed=base_seed if use_temporal_coherence else seed # get_tensor_field handles seed internally based on coherence
+ )
+
+ # Reset random seed state to not affect other operations
+ torch.manual_seed(torch.seed())
+
+ # get_tensor_field is now responsible for the correct number of channels.
+ # The fixed expansion to 4 channels here is removed.
+
+ # Normalize the noise to match typical noise distribution
+ # This should be done carefully if the output from get_tensor_field is already in a specific range (e.g. -1 to 1)
+ # Assuming get_tensor_field output is [-1, 1], normalization might alter this.
+ # For now, keeping the normalization as it was in the original file.
+ noise_bhwc = (noise_bhwc - noise_bhwc.mean()) / (noise_bhwc.std() + 1e-8)
+
+ # debugger.track_tensor_shape_history(noise_bhwc, "noise_bhwc_normalized_output", "generate_tensor_field_tensor_output")
+ return noise_bhwc
+
+def add_temporal_coherent_tensor_field_to_shader_params(shader_params, animation_frame, base_seed, fps=24, duration=1.0, phase_shift=0.0, use_temporal_coherence=True, color_scheme="none", color_intensity=0.8):
+ """
+ Helper function to add temporal coherence parameters to shader_params for tensor field
+
+ Args:
+ shader_params: Dictionary containing shader parameters
+ animation_frame: Current frame in the animation
+ base_seed: Base seed to use for all frames
+ fps: Frames per second of the animation (default: 24)
+ duration: Total duration of the animation in seconds (default: 1.0)
+ phase_shift: Phase shift to apply to time calculation (default: 0.0)
+ use_temporal_coherence: Whether to use temporal coherence (default: True)
+ color_scheme: Color scheme to use (default: "none")
+ color_intensity: Intensity of the color effect (default: 0.8)
+
+ Returns:
+ Updated shader_params with temporal coherence parameters
+ """
+ # Calculate total frames
+ animation_length = int(fps * duration)
+
+ # Calculate normalized time value between 0.0 and 1.0
+ time = animation_frame / max(animation_length - 1, 1)
+
+ # Apply phase shift (loops from 0 to 1)
+ time = (time + phase_shift) % 1.0
+
+ # Add parameters to shader_params
+ shader_params["time"] = time
+ shader_params["base_seed"] = base_seed
+ shader_params["temporal_coherence"] = use_temporal_coherence
+
+ # Add color scheme parameters if specified
+ if color_scheme != "none":
+ shader_params["colorScheme"] = color_scheme
+ shader_params["shaderColorIntensity"] = color_intensity
+ # print(f"Added color scheme: {color_scheme} with intensity {color_intensity}") # Removed this line
+
+ # print(f"Tensor field temporal settings: frame={animation_frame}/{animation_length}, time={time:.3f}, base_seed={base_seed}, coherence={use_temporal_coherence}") # Removed this line
+
+ # debugger = get_debugger()
+ # debugger.track_tensor_shape_history(shader_params, "shader_params_with_temporal", "add_temporal_coherent_params_output")
+ return shader_params
+
+def generate_tensor_field_animation_frames(output_dir, width, height, frame_count,
+ scale=1.0, warp_strength=0.5, phase_shift=0.0,
+ visualization_type=3,
+ shape_type="none", shape_mask_strength=1.0,
+ fps=24, duration=1.0, device="cuda", seed=42,
+ color_scheme="none", color_intensity=0.8):
+ """
+ Generate a sequence of tensor field animation frames
+
+ Args:
+ output_dir: Directory to save frames to
+ width: Width of frames
+ height: Height of frames
+ frame_count: Number of frames to generate
+ scale: Scale parameter
+ warp_strength: Warp strength parameter
+ phase_shift: Phase shift parameter
+ visualization_type: Type of tensor field visualization (0-3)
+ shape_type: Shape mask type
+ shape_mask_strength: Shape mask strength
+ fps: Frames per second
+ duration: Animation duration in seconds
+ device: Device to use for computation
+ seed: Random seed for deterministic results
+ color_scheme: Color scheme to use
+ color_intensity: Color intensity
+
+ Returns:
+ List of paths to generated frames
+ """
+ import os
+ import torch
+ from PIL import Image
+ import numpy as np
+
+ # Create output directory if it doesn't exist
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Frame paths
+ frame_paths = []
+
+ # Set octaves to control visualization type (same as in shader)
+ octaves = visualization_type % 4
+
+ # Base shader parameters
+ shader_params = {
+ "shaderScale": scale,
+ "shaderWarpStrength": warp_strength,
+ "shaderPhaseShift": phase_shift,
+ "shaderOctaves": octaves,
+ "shaderShapeType": shape_type,
+ "shaderShapeStrength": shape_mask_strength
+ }
+
+ # debugger = get_debugger()
+ # debugger.track_tensor_shape_history(shader_params, "base_shader_params_animation_frames", "animation_frame_generation_start")
+
+ # print(f"Generating {frame_count} tensor field animation frames with:") # Removed this line
+ # print(f"Parameters: scale={scale}, warp={warp_strength}, phase={phase_shift}") # Removed this line
+ # print(f"Visualization type: {visualization_type}") # Removed this line
+ # print(f"Color settings: scheme={color_scheme}, intensity={color_intensity}") # Removed this line
+ # print(f"Output size: {width}x{height}") # Removed this line
+
+ # Generate frames
+ for frame in range(frame_count):
+ # print(f"Generating frame {frame+1}/{frame_count}...") # Removed this line
+
+ # Add temporal parameters to shader_params
+ frame_params = add_temporal_coherent_tensor_field_to_shader_params(
+ shader_params.copy(), frame, seed, fps, duration, 0.0, True,
+ color_scheme, color_intensity
+ )
+ # debugger.track_tensor_shape_history(frame_params, f"frame_params_animation_frame_{frame}", "animation_frame_params_update")
+
+ # Generate noise
+ tensor_field = TensorFieldGenerator.get_tensor_field(
+ batch_size=1,
+ height=height,
+ width=width,
+ shader_params=frame_params,
+ device=device,
+ seed=seed
+ )
+
+ # debugger.track_tensor_shape_history(tensor_field, f"tensor_field_generated_animation_frame_{frame}", "animation_frame_tensor_generation")
+
+ # Convert to image
+ # Scale from [-1, 1] to [0, 1]
+ tensor_field = (tensor_field + 1.0) * 0.5
+
+ # Convert to numpy array and then to PIL Image
+ img_np = tensor_field[0].permute(1, 2, 0).cpu().numpy()
+
+ # Convert to uint8 for saving as image
+ img_np = (img_np * 255).astype(np.uint8)
+
+ # Create PIL image (use RGB channels only)
+ img = Image.fromarray(img_np[:, :, :3])
+
+ # Save the image
+ frame_path = os.path.join(output_dir, f"tensor_field_frame_{frame:04d}.png")
+ img.save(frame_path)
+ frame_paths.append(frame_path)
+
+ # print(f"Generated {len(frame_paths)} frames in {output_dir}") # Removed this line
+ return frame_paths
+
+# Integration with ShaderToTensor class
+def add_tensor_field_to_tensor(tensor_class):
+ """
+ Add tensor field implementation to ShaderToTensor class
+ Call this function to monkey patch the ShaderToTensor class
+
+ Args:
+ tensor_class: The ShaderToTensor class to modify
+ """
+ def tensor_field(cls, p, time, scale, warp_strength, phase_shift, octaves=3.0, seed=0, shape_type="none", shape_mask_strength=1.0):
+ """
+ Tensor field implementation for ShaderToTensor
+
+ Args:
+ p: Coordinate tensor [batch, height, width, 2]
+ time: Animation time
+ scale: Scale factor
+ warp_strength: Warp amount
+ phase_shift: Contrast adjustment
+ octaves: Controls visualization type
+ seed: Random seed
+ shape_type: Type of shape mask to apply ("none" for no mask)
+ shape_mask_strength: Strength of shape mask application
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ # Create parameter dictionary from individual params
+ shader_params = {
+ "shaderScale": scale,
+ "shaderWarpStrength": warp_strength,
+ "shaderPhaseShift": phase_shift,
+ "shaderOctaves": octaves,
+ "time": time,
+ "shaderShapeType": shape_type,
+ "shaderShapeStrength": shape_mask_strength
+ }
+
+ # Get shape from p
+ batch, height, width, _ = p.shape
+
+ # Use the TensorFieldGenerator directly
+ tensor_noise = TensorFieldGenerator.get_tensor_field(
+ batch_size=batch,
+ height=height,
+ width=width,
+ shader_params=shader_params,
+ device=p.device,
+ seed=seed # Pass the seed to the generator
+ )
+
+ # Convert from [B, 4, H, W] to [B, H, W, 1] for consistency with other functions
+ return tensor_noise.permute(0, 2, 3, 1)[:, :, :, 0:1]
+
+ # Add the method to the class
+ setattr(tensor_class, 'tensor_field', classmethod(tensor_field))
+
+ def tensor_field_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ """
+ New method that directly takes the full shader_params dictionary
+
+ Args:
+ batch_size: Number of images in batch
+ height: Height of tensor
+ width: Width of tensor
+ shader_params: Full parameter dictionary from JSON
+ time: Animation time
+ device: Device to create tensor on
+ seed: Random seed
+
+ Returns:
+ Noise tensor [batch, height, width, 1]
+ """
+ # print(f">>> tensor_field_with_params called with full params dictionary") # Removed
+ # print(f">>> Raw params: {shader_params}") # Removed
+
+ # Add time to shader params if not already present
+ if "time" not in shader_params:
+ shader_params["time"] = time
+
+ # Ensure shape parameters are properly extracted and included
+ shape_type = shader_params.get("shaderShapeType", shader_params.get("shape_type", "none"))
+ shape_mask_strength = shader_params.get("shaderShapeStrength", shader_params.get("shapemaskstrength", 1.0))
+
+ # Make sure the correct parameter names are set for consistency
+ shader_params["shaderShapeType"] = shape_type
+ shader_params["shaderShapeStrength"] = shape_mask_strength
+
+ # print(f">>> Shape mask params: type={shape_type}, strength={shape_mask_strength}") # Removed
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(0, 1, height, device=device),
+ torch.linspace(0, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Set deterministic seed to ensure consistent results
+ torch.manual_seed(seed)
+
+ # Use the TensorFieldGenerator directly with the full params
+ tensor_noise = TensorFieldGenerator.get_tensor_field(
+ batch_size=batch_size,
+ height=height,
+ width=width,
+ shader_params=shader_params, # Pass the full parameter dictionary
+ device=device,
+ seed=seed
+ )
+
+ # Convert to BHWC format to match expected output
+ result = tensor_noise.permute(0, 2, 3, 1)[:, :, :, 0:1]
+ # print(f">>> tensor_field_with_params produced tensor shape: {result.shape}") # Removed
+
+ # debugger.track_tensor_shape_history(result, "result_tf_with_params_output", "tensor_field_with_params_output")
+
+ return result
+
+ # Add the new method to the class
+ setattr(tensor_class, 'tensor_field_with_params', classmethod(tensor_field_with_params))
+
+ # print("=== Added tensor_field functionality to ShaderToTensor ===") # Removed this line
+
+ # Update shader_noise_to_tensor to handle tensor_field type
+ original_shader_noise_to_tensor = tensor_class.shader_noise_to_tensor
+
+ @classmethod
+ def new_shader_noise_to_tensor(cls, batch_size=1, height=64, width=64, shader_type="tensor_field",
+ visualization_type=3, scale=1.0, phase_shift=0.0,
+ warp_strength=0.0, time=0.0, device="cuda", seed=0, octaves=3.0,
+ shader_params=None, shape_type="none", shape_mask_strength=1.0):
+ """Updated shader_noise_to_tensor with tensor_field support"""
+ # print(f"Generating {shader_type} noise with scale={scale}, octaves={octaves}, warp={warp_strength}, phase={phase_shift}") # Removed
+ # print(f"Shape params: type={shape_type}, strength={shape_mask_strength}") # Removed
+
+ # Check if we have a shader_params dictionary passed
+ has_shader_params = shader_params is not None
+ # if has_shader_params: # Removed
+ # print(f"Full shader_params provided: {shader_params}") # Removed
+
+ # Handle tensor_field shader type explicitly
+ if shader_type == "tensor_field":
+ # print("EXECUTING TENSOR_FIELD BRANCH") # Removed
+
+ # If we have the full params dict and the new method, use it
+ if has_shader_params and hasattr(cls, 'tensor_field_with_params'):
+ # Ensure shape parameters are in the shader_params
+ if "shaderShapeType" not in shader_params and "shape_type" not in shader_params:
+ shader_params["shaderShapeType"] = shape_type
+ if "shaderShapeStrength" not in shader_params and "shapemaskstrength" not in shader_params:
+ shader_params["shaderShapeStrength"] = shape_mask_strength
+
+ return cls.tensor_field_with_params(
+ batch_size, height, width, shader_params, time, device, seed
+ )
+
+ # Otherwise use the original implementation
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(0, 1, height, device=device),
+ torch.linspace(0, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Set deterministic seed to ensure consistent results with same seed
+ torch.manual_seed(seed)
+
+ # Generate tensor field noise
+ result = cls.tensor_field(
+ p, time, scale, warp_strength, phase_shift, octaves, seed, shape_type, shape_mask_strength
+ )
+
+ # print(f"Tensor field noise raw result shape: {result.shape}") # Removed
+
+ # Ensure result has the right shape [batch, height, width, 1]
+ if len(result.shape) == 3: # Missing dimension
+ result = result.unsqueeze(-1)
+
+ # print(f"Final tensor field noise shape: {result.shape}") # Removed
+
+ # debugger.track_tensor_shape_history(result, "result_new_shader_noise_to_tensor_tf_branch", "new_shader_noise_to_tensor_tf_output")
+
+ return result
+ else:
+ # Call original method for other shader types
+ return original_shader_noise_to_tensor(
+ batch_size, height, width, shader_type, visualization_type,
+ scale, phase_shift, warp_strength, time, device, seed, octaves,
+ shader_params, shape_type, shape_mask_strength
+ )
+
+ # Replace the original method only if it hasn't been replaced by other noise integrations already
+ if not hasattr(tensor_class, 'shader_noise_to_tensor'):
+ setattr(tensor_class, 'shader_noise_to_tensor', new_shader_noise_to_tensor)
+ else:
+ # If already replaced, we need to update the existing method to handle tensor_field
+ existing_method = tensor_class.shader_noise_to_tensor
+
+ @classmethod
+ def updated_shader_noise_to_tensor(cls, batch_size=1, height=64, width=64, shader_type="tensor_field",
+ visualization_type=3, scale=1.0, phase_shift=0.0,
+ warp_strength=0.0, time=0.0, device="cuda", seed=0, octaves=3.0,
+ shader_params=None, shape_type="none", shape_mask_strength=1.0):
+ # Handle tensor_field shader type explicitly
+ if shader_type == "tensor_field":
+ # print("EXECUTING TENSOR_FIELD BRANCH") # Removed
+
+ # If we have the full params dict and the new method, use it
+ has_shader_params = shader_params is not None
+ if has_shader_params:
+ # Ensure shape parameters are in the shader_params
+ if "shaderShapeType" not in shader_params and "shape_type" not in shader_params:
+ shader_params["shaderShapeType"] = shape_type
+ if "shaderShapeStrength" not in shader_params and "shapemaskstrength" not in shader_params:
+ shader_params["shaderShapeStrength"] = shape_mask_strength
+
+ return cls.tensor_field_with_params(
+ batch_size, height, width, shader_params, time, device, seed
+ )
+
+ # Otherwise use the original implementation
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(0, 1, height, device=device),
+ torch.linspace(0, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Set deterministic seed
+ torch.manual_seed(seed)
+
+ # Generate tensor field noise
+ result = cls.tensor_field(
+ p, time, scale, warp_strength, phase_shift, octaves, seed, shape_type, shape_mask_strength
+ )
+
+ # Ensure result has the right shape
+ if len(result.shape) == 3:
+ result = result.unsqueeze(-1)
+
+ # print(f"Final tensor field noise shape: {result.shape}") # Removed
+
+ # debugger.track_tensor_shape_history(result, "result_updated_shader_noise_to_tensor_tf_branch", "updated_shader_noise_to_tensor_tf_output")
+ return result
+ else:
+ # Call existing method for other shader types
+ return existing_method(
+ batch_size, height, width, shader_type, visualization_type,
+ scale, phase_shift, warp_strength, time, device, seed, octaves,
+ shader_params, shape_type, shape_mask_strength
+ )
+
+ setattr(tensor_class, 'shader_noise_to_tensor', updated_shader_noise_to_tensor)
+
+def integrate_tensor_field():
+ """
+ Integrate tensor field implementation into existing code
+ This should be called in __init__.py to ensure tensor field is available
+ """
+ # Import the necessary classes
+ from ..shader_to_tensor import ShaderToTensor
+
+ # Add the tensor field implementation to ShaderToTensor
+ add_tensor_field_to_tensor(ShaderToTensor)
+
+ # print("Tensor field implementation integrated successfully") # Removed this line
\ No newline at end of file
diff --git a/web/gradient_title.js b/web/gradient_title.js
new file mode 100644
index 0000000..4bda1a1
--- /dev/null
+++ b/web/gradient_title.js
@@ -0,0 +1,245 @@
+// gradient_title.js - Adds a custom gradient title to ShaderNoiseKSampler node
+
+import { app } from "../../scripts/app.js";
+
+// Cache for rendering optimization
+const CACHE = {
+ titleCanvas: null,
+ titleCtx: null,
+ lastWidth: 0,
+ lastHeight: 0,
+ lastTime: 0,
+ frameCount: 0,
+ frameSkip: 2, // Only update animation every X frames
+ collapsed: {
+ canvas: null,
+ ctx: null,
+ lastWidth: 0
+ }
+};
+
+// Register a callback to run when ComfyUI is fully loaded
+app.registerExtension({
+ name: "ShaderNoiseKSampler.GradientTitle",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ // Apply to both shader noise ksampler nodes
+ if (nodeData.name === "ShaderNoiseKSampler" || nodeData.name === "ShaderNoiseKSamplerDirect") {
+ // Store the original onDrawForeground function if it exists
+ const origOnDrawForeground = nodeType.prototype.onDrawForeground;
+
+ // Add our own onDrawForeground function
+ nodeType.prototype.onDrawForeground = function(ctx) {
+ // Call the original onDrawForeground if it exists
+ if (origOnDrawForeground) {
+ origOnDrawForeground.apply(this, arguments);
+ }
+
+ // Draw a custom gradient title
+ drawGradientTitle(this, ctx);
+ };
+
+ // Clean up resources when node is removed
+ const origOnRemoved = nodeType.prototype.onRemoved;
+ nodeType.prototype.onRemoved = function() {
+ if (origOnRemoved) {
+ origOnRemoved.apply(this, arguments);
+ }
+
+ // Clear cached canvases to prevent memory leaks
+ CACHE.titleCanvas = null;
+ CACHE.titleCtx = null;
+ CACHE.collapsed.canvas = null;
+ CACHE.collapsed.ctx = null;
+ };
+ }
+ }
+});
+
+/**
+ * Draws a gradient title directly on the canvas
+ * @param {LGraphNode} node - The node to apply the gradient to
+ * @param {CanvasRenderingContext2D} ctx - The canvas context
+ */
+function drawGradientTitle(node, ctx) {
+ // Get title area dimensions
+ const titleHeight = node.flags.collapsed ? 20 : 30; // Smaller height when collapsed
+ const width = node.flags.collapsed ? 190 : node.size[0]; // Smaller width when collapsed
+ const fullHeight = node.size[1]; // Get actual node height
+ const equationY = 45; // Y position for the equation, moved lower
+
+ // Choose appropriate equation based on node type
+ let equation, collapsedEquation;
+
+ if (node.type === "ShaderNoiseKSamplerDirect") {
+ equation = "Lt = Sฮฑ(N) โ Kฮฒ(t) โฟ";
+ collapsedEquation = "Lt = Sฮฑ(N) โ Kฮฒ(t) โฟ";
+ } else {
+ equation = "Lt = Sฮฑ(N) โ Kฮฒ(t)";
+ collapsedEquation = "Lt = Sฮฑ(N) โ Kฮฒ(t)";
+ }
+
+ // Update animation frame counter (performance optimization)
+ CACHE.frameCount = (CACHE.frameCount + 1) % (CACHE.frameSkip + 1);
+ const shouldUpdateAnimation = CACHE.frameCount === 0;
+
+ // Save current state
+ ctx.save();
+
+ // Reset shadow properties for gradient drawing
+ ctx.shadowColor = "transparent";
+ ctx.shadowBlur = 0;
+ ctx.shadowOffsetX = 0;
+ ctx.shadowOffsetY = 0;
+
+ // Create vertical black gradient for entire background that fills node height
+ const gradient = ctx.createLinearGradient(0, 0, 0, fullHeight);
+ gradient.addColorStop(0, "#000000"); // Pure black at top
+ gradient.addColorStop(0.2, "#101010"); // Transition to very dark gray
+ gradient.addColorStop(1, "#101010"); // Very dark gray at bottom
+
+ // Create smooth shimmer effect for mathematical formula - only calculate if animation should update
+ let shimmerPosition = 0.5; // Default middle position
+ if (shouldUpdateAnimation) {
+ const time = Date.now() / 3000; // Faster time factor (changed from 4000)
+ shimmerPosition = (Math.sin(time) + 1) / 2; // Changed from cos to sin for left-to-right only
+ // Store for later use if needed
+ CACHE.lastTime = time;
+ } else {
+ // Reuse last calculation for animation frames we're skipping
+ const time = CACHE.lastTime || Date.now() / 3000;
+ shimmerPosition = (Math.sin(time) + 1) / 2;
+ }
+
+ // Add collapse button handler
+ if (node.flags.collapsed) {
+ // If node is collapsed, adjust the title rendering
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, width, titleHeight);
+
+ // Draw etched shadow for collapsed version
+ ctx.fillStyle = "rgba(0,0,0,0.3)";
+ ctx.font = "italic 11px Arial"; // Smaller font for collapsed state
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText(collapsedEquation, width / 2 + 1, titleHeight / 2 + 1);
+
+ // Create base golden gradient for collapsed version
+ const baseGradient = ctx.createLinearGradient(0, titleHeight/2 - 5, 0, titleHeight/2 + 5);
+ baseGradient.addColorStop(0, "#B8860B"); // Darker gold
+ baseGradient.addColorStop(0.5, "#FFD700"); // Bright gold
+ baseGradient.addColorStop(1, "#B8860B"); // Darker gold
+
+ // Draw base golden text
+ ctx.fillStyle = baseGradient;
+ ctx.font = "italic 11px Arial";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText(collapsedEquation, width / 2, titleHeight / 2);
+
+ // Create moving highlight effect for collapsed version
+ const highlightWidth = width * 0.4; // Width of the highlight
+ const highlightX = -highlightWidth + (width + highlightWidth) * shimmerPosition; // Adjusted range
+
+ const shimmerGradient = ctx.createLinearGradient(
+ highlightX - highlightWidth/2, 0,
+ highlightX + highlightWidth/2, 0
+ );
+
+ // Create smooth highlight transition
+ shimmerGradient.addColorStop(0, "rgba(255, 255, 200, 0)");
+ shimmerGradient.addColorStop(0.1, "rgba(255, 255, 200, 0)");
+ shimmerGradient.addColorStop(0.5, "rgba(255, 255, 200, 0.3)");
+ shimmerGradient.addColorStop(0.9, "rgba(255, 255, 200, 0)");
+ shimmerGradient.addColorStop(1, "rgba(255, 255, 200, 0)");
+
+ // Apply highlight
+ ctx.fillStyle = shimmerGradient;
+ ctx.fillText(collapsedEquation, width / 2, titleHeight / 2);
+
+ // Add outline glow that follows the highlight
+ const glowIntensity = Math.max(0, 1 - Math.abs(width/2 - highlightX)/(width/4));
+ ctx.shadowColor = `rgba(255, 255, 200, ${glowIntensity * 0.3})`;
+ ctx.shadowBlur = 4; // Less blur for collapsed version
+ ctx.shadowOffsetX = 0;
+ ctx.shadowOffsetY = 0;
+ ctx.fillText(collapsedEquation, width / 2, titleHeight / 2);
+
+ // Skip the rest of the rendering when collapsed
+ ctx.restore();
+ return;
+ }
+
+ // Draw background that fills the entire node
+ ctx.fillStyle = gradient;
+
+ // Use rounded rectangle for the background with rounded corners at the bottom
+ if (!node.flags.collapsed) {
+ const cornerRadius = 8; // Adjust radius as needed
+
+ // Create path for rounded rectangle
+ ctx.beginPath();
+ ctx.moveTo(0, 0); // Top-left corner (no rounding)
+ ctx.lineTo(width, 0); // Top-right corner (no rounding)
+ ctx.lineTo(width, fullHeight - cornerRadius); // Right edge before bottom-right corner
+ ctx.arcTo(width, fullHeight, width - cornerRadius, fullHeight, cornerRadius); // Bottom-right rounded corner
+ ctx.lineTo(cornerRadius, fullHeight); // Bottom edge before bottom-left corner
+ ctx.arcTo(0, fullHeight, 0, fullHeight - cornerRadius, cornerRadius); // Bottom-left rounded corner
+ ctx.lineTo(0, 0); // Left edge back to top
+ ctx.closePath();
+ ctx.fill();
+ } else {
+ // Keep regular rectangle for collapsed state
+ ctx.fillRect(0, 0, width, fullHeight); // No extra padding
+ }
+
+ // Draw etched shadow
+ ctx.fillStyle = "rgba(0,0,0,0.3)";
+ ctx.font = "italic 14px Arial"; // Smaller font for equation
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText(equation, width / 2 + 2, equationY + 2);
+
+ // Create base golden gradient
+ const baseGradient = ctx.createLinearGradient(0, equationY - 7, 0, equationY + 7);
+ baseGradient.addColorStop(0, "#B8860B"); // Darker gold
+ baseGradient.addColorStop(0.5, "#FFD700"); // Bright gold
+ baseGradient.addColorStop(1, "#B8860B"); // Darker gold
+
+ // Draw base golden text
+ ctx.fillStyle = baseGradient;
+ ctx.font = "italic 14px Arial"; // Smaller font for equation
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText(equation, width / 2, equationY);
+
+ // Create moving highlight effect
+ const highlightWidth = width * 0.4; // Width of the highlight
+ const highlightX = -highlightWidth + (width + highlightWidth) * shimmerPosition; // Adjusted range
+
+ const shimmerGradient = ctx.createLinearGradient(
+ highlightX - highlightWidth/2, 0,
+ highlightX + highlightWidth/2, 0
+ );
+
+ // Create smooth highlight transition
+ shimmerGradient.addColorStop(0, "rgba(255, 255, 200, 0)");
+ shimmerGradient.addColorStop(0.1, "rgba(255, 255, 200, 0)");
+ shimmerGradient.addColorStop(0.5, "rgba(255, 255, 200, 0.3)");
+ shimmerGradient.addColorStop(0.9, "rgba(255, 255, 200, 0)");
+ shimmerGradient.addColorStop(1, "rgba(255, 255, 200, 0)");
+
+ // Apply highlight
+ ctx.fillStyle = shimmerGradient;
+ ctx.fillText(equation, width / 2, equationY);
+
+ // Add outline glow that follows the highlight
+ const glowIntensity = Math.max(0, 1 - Math.abs(width/2 - highlightX)/(width/4));
+ ctx.shadowColor = `rgba(255, 255, 200, ${glowIntensity * 0.3})`;
+ ctx.shadowBlur = 8;
+ ctx.shadowOffsetX = 0;
+ ctx.shadowOffsetY = 0;
+ ctx.fillText(equation, width / 2, equationY);
+
+ // Restore context state
+ ctx.restore();
+}
\ No newline at end of file
diff --git a/web/images/kofi_symbol.svg b/web/images/kofi_symbol.svg
new file mode 100644
index 0000000..ade749d
--- /dev/null
+++ b/web/images/kofi_symbol.svg
@@ -0,0 +1,13 @@
+
diff --git a/web/images/support_me_on_kofi_badge_blue.png b/web/images/support_me_on_kofi_badge_blue.png
new file mode 100644
index 0000000..97cacb4
Binary files /dev/null and b/web/images/support_me_on_kofi_badge_blue.png differ
diff --git a/web/matrix_button.js b/web/matrix_button.js
new file mode 100644
index 0000000..0e9a0f7
--- /dev/null
+++ b/web/matrix_button.js
@@ -0,0 +1,3095 @@
+import { app } from "../../scripts/app.js";
+
+/**
+ * Adds a "Show Matrix" button to the ShaderDisplay and ShaderNoiseKSampler nodes
+ */
+(function() {
+ // Define utility functions on window object to be accessible by treatiseHTML
+ window.scrollToSection = function(sectionId) {
+ const modalContent = document.querySelector('.shader-matrix-treatise'); // Scroll within the modal
+ if (!modalContent) return;
+ const section = modalContent.querySelector('#' + sectionId);
+ if (section) {
+ section.scrollIntoView({ behavior: 'smooth' });
+ }
+ };
+
+ window.showTab = function(tabIdToActivate, clickedTabElement) {
+ let tabsContainer;
+ let contentScope;
+ let tabSelector;
+ let tabContentSelector;
+
+ if (clickedTabElement) { // Prioritize if element is passed
+ tabsContainer = clickedTabElement.closest('.tabs') || clickedTabElement.closest('.sacred-tabs');
+ } else { // Fallback to querying globally within the modal if no element
+ const modalDiv = document.querySelector('.shader-matrix-treatise');
+ if (!modalDiv) return;
+ tabsContainer = modalDiv.querySelector('.tabs') || modalDiv.querySelector('.sacred-tabs');
+ }
+
+ if (!tabsContainer) {
+ console.error("showTab: Could not find '.tabs' or '.sacred-tabs' container.");
+ return;
+ }
+
+ if (tabsContainer.classList.contains('tabs')) {
+ tabSelector = '.tab';
+ tabContentSelector = '.tab-content';
+ contentScope = tabsContainer.parentNode; // Assumes content is sibling to .tabs div
+ } else if (tabsContainer.classList.contains('sacred-tabs')) {
+ tabSelector = '.sacred-tab';
+ tabContentSelector = '.sacred-tab-content';
+ contentScope = tabsContainer.closest('.sacred-section') || tabsContainer.parentNode;
+ } else {
+ return; // Unknown tab structure
+ }
+
+ tabsContainer.querySelectorAll(tabSelector).forEach(tab => tab.classList.remove('active'));
+ if (clickedTabElement) {
+ clickedTabElement.classList.add('active');
+ } else {
+ // If no clicked element, try to find the tab by tabId (less robust)
+ const targetTab = Array.from(tabsContainer.querySelectorAll(tabSelector)).find(
+ t => t.getAttribute('onclick') && t.getAttribute('onclick').includes(tabIdToActivate)
+ );
+ if (targetTab) targetTab.classList.add('active');
+ }
+
+ if (contentScope) {
+ contentScope.querySelectorAll(tabContentSelector).forEach(content => {
+ content.style.display = 'none';
+ content.classList.remove('active');
+ });
+ const activeContent = contentScope.querySelector('#' + tabIdToActivate);
+ if (activeContent) {
+ activeContent.style.display = 'block';
+ activeContent.classList.add('active');
+ }
+ }
+ };
+
+ window.setupScrollTop = function(modalContentElement) { // Added modalContentElement parameter
+ if (!modalContentElement) return; // Check the passed element
+ const scrollTopButton = modalContentElement.querySelector('#scroll-top'); // Use passed element
+
+ if (scrollTopButton) {
+ modalContentElement.addEventListener('scroll', () => { // Use passed element
+ if (modalContentElement.scrollTop > 200) { // Use passed element
+ scrollTopButton.classList.add('visible');
+ } else {
+ scrollTopButton.classList.remove('visible');
+ }
+ });
+ scrollTopButton.addEventListener('click', (e) => {
+ e.stopPropagation();
+ modalContentElement.scrollTo({ top: 0, behavior: 'smooth' });
+ });
+ }
+ };
+
+ window.toggleCodeSection = function(buttonElement) {
+ const headerElement = buttonElement.closest('.code-block-header');
+ if (!headerElement) return;
+ const codeBlockContainer = headerElement.parentNode;
+ if (!codeBlockContainer) return;
+
+ const preElement = codeBlockContainer.querySelector('pre.foldable-content');
+ if (!preElement) return;
+
+ const isHidden = preElement.style.display === 'none' || preElement.style.display === '';
+
+ if (isHidden) {
+ preElement.style.display = 'block';
+ buttonElement.textContent = 'Hide';
+ } else {
+ preElement.style.display = 'none';
+ buttonElement.textContent = 'Show';
+ }
+ };
+
+ // Register the extension for ShaderDisplay and ShaderNoiseKSampler nodes
+ app.registerExtension({
+ name: "ComfyUI.ShaderNoise.MatrixButton",
+
+ beforeRegisterNodeDef(nodeType, nodeData) {
+ // Modify ShaderDisplay, ShaderNoiseKSampler, and ShaderNoiseKSamplerDirect nodes
+ if (nodeData.name !== "ShaderDisplay" && nodeData.name !== "ShaderNoiseKSampler" && nodeData.name !== "ShaderNoiseKSamplerDirect") {
+ return;
+ }
+
+ // Store the original methods
+ const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
+
+ // Add our button to the node
+ nodeType.prototype.onNodeCreated = function() {
+ // Call the original onNodeCreated method first
+ const self = this; // Node instance
+
+ if (originalOnNodeCreated) {
+ originalOnNodeCreated.apply(self, arguments);
+ }
+
+ // Add the "Show Matrix" button widget
+ // const self = this; // self is already defined above
+
+ // Function to add the matrix button
+ const addMatrixButton = () => {
+ // MODIFIED: HTML content from Treatise.js
+ // window.showTab_SacredCodex = function(tabId, clickedTabElement) { ... };
+ // This ^ is now replaced by the global window.showTab for new HTML,
+ // but can be kept if old structures outside this modal might use it.
+ // For this modal, the new HTML will use window.showTab.
+
+ const button = self.addWidget("button", "๐ Show Shader Matrix", null, function() {
+ // Create modal container
+ const modal = document.createElement("div");
+ modal.style.cssText = `
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
+ background: linear-gradient(135deg, rgba(0,0,0,0.9), rgba(26,13,52,0.95));
+ display: flex; justify-content: center; align-items: center; z-index: 10000;
+ backdrop-filter: blur(5px);
+ `;
+
+ let handleEscPress;
+
+ const closeModalCleanup = () => {
+ if (modal && modal.parentNode) {
+ document.body.removeChild(modal);
+ }
+ if (handleEscPress) {
+ document.removeEventListener('keydown', handleEscPress);
+ }
+ };
+
+ handleEscPress = (e) => {
+ if (e.key === "Escape") {
+ closeModalCleanup();
+ }
+ };
+
+ document.addEventListener('keydown', handleEscPress);
+
+ modal.onclick = (e) => {
+ if (e.target === modal) {
+ closeModalCleanup();
+ }
+ };
+
+ const content = document.createElement("div");
+ content.className = "shader-matrix-treatise"; // This is the main scrollable container
+ content.style.cssText = `
+ position: relative; /* For positioning of child elements like scroll-to-top */
+ background: linear-gradient(145deg, #0f0f12, #1a1a2e); /* Match new body bg */
+ border-radius: 16px; padding: 0; /* Remove padding, let content manage it */
+ max-width: 95vw; width: 1400px; /* Wider for new content */
+ max-height: 90vh; overflow-y: auto; color: #e0e0e8; /* Match new text color */
+ box-shadow: 0 0 60px rgba(138, 43, 226, 0.5);
+ border: 1px solid rgba(138, 43, 226, 0.4);
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Base font */
+ line-height: 1.6;
+ `;
+
+ // MODIFIED: HTML content from Treatise.js
+ const treatiseHTML = `
+
+
+
+
+
+
+
+
+
The Shader Matrix
+
Harnessing Noise with Shader Algorithms for Image Generation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
๐ฎ Introduction โจ The Essence of Noise
+
ShaderNoiseKSampler is a ComfyUI node that uses shader algorithms to generate and control procedural noise. This noise, refeered to as shader noise, can then be used to influence the AI image creation process in latent diffusion models. The aim is to provide a way to introduce structured mathematical patterns into the diffusion process, offering a different approach to guiding image generation.
+
This document outlines the features, technical details, and underlying ideas of ShaderNoiseKSampler, showing how different shader noise types can create various visual characteristics that influence and drive the diffusion process.
+
+
+
+
+
+
+
๐ The Path of Noise
+
Noise is fundamentally a mathematical expression of ordered chaosโa seemingly random pattern that nonetheless follows precise mathematical rules. In the context of image generation, these noise patterns serve as the seed from which creation emerges. ShaderNoiseKSampler allows artists to shape this primordial mathematical chaos according to sacred geometric principles, guiding the diffusion model\'s sampling process along specific aesthetic trajectories.
+
Just as ancient civilizations recognized patterns in nature and encoded them in their art and architecture, ShaderNoiseKSampler enables digital artists to encode mathematical archetypes into the generative process, creating a bridge between abstract mathematics and visual expression.
+
+
+
+
+
+
+
๐ Project Genesis โThe Birth of Order from Chaos
+
+
+
๐๏ธ The Vision Unveiled
+
The genesis of ShaderNoiseKSampler lies in a contemplation of nature\'s fundamental paradox: the inherent order within apparent chaos. Observing the natural world reveals that seemingly random phenomenaโthe flicker of fire, the branching of lightning, the flow of water, the structure of terrestrial formsโall exhibit underlying fractal patterns. This ubiquitous principle, that profound order underpins apparent disorder, became a guiding inspiration.
+
The initial conceptual spark was hyperspaceโan unseen realm between dimensions. While seemingly chaotic in its construct, closer examination reveals a perfect, almost unimaginable mathematical precision and scope. It is this notion that the unseen can be rendered observable with the appropriate tools which led to a pivotal question: Could a method be devised to introduce deliberate order into random noise? Could one inject structure into the latent space itself, thereby crystallizing defined pathways for creative generation?
+
+
+
+
๐๏ธ The Ant Colony Revelation
+
A compelling metaphor emerged from an unexpected source: the art of ant sculpture. This process, which captures the hidden architecture of ant tunnels by filling them with molten metal, offered a tangible analogy. Once cooled and excavated, the metal cast reveals a previously unseen, complex structureโa physical manifestation of the colony\'s hidden pathways. The question then became: how could one create a virtual equivalent of this molten metal? The answer was clear: Shaders.
+
+
+
Virtual Molten Metal (Shaders) = Shader Noise Shader patterns, like molten metal, flow into and define the pathways within latent space.
+
Solidification = Sampling Process The K-sampling process allows this structured noise to effectively \'solidify\' within the latent dimensions.
+
Excavation = Denoising As the model denoises, it progressively reveals the crystallized patterns embedded by the shader.
+
Revealed Sculpture = Final Image The output is a manifestation of a previously unseen space, a navigable pathway forged through the realm of possibility.
+
+
+
+
โ๏ธ The Alchemical Process: From Theory to Practice
+
The journey from concept to a functional implementation involved extensive experimentation, iterative debugging, and countless visualization tests. The challenge was multifaceted, encompassing not only technical hurdles but also philosophical considerations: How could the abstract vision of controlled chaos be translated into robust code? Critically, how could novel structures be introduced without disrupting the model\'s foundational training?
+
A crucial breakthrough was the understanding that models cannot be expected to produce coherent results when fed noise types entirely alien to their training data. This led to the principle of augmentation rather than replacement. The final noise input to the KSampler is therefore not a wholesale substitution but a careful modification of the standard base noise. The shader noise augments and sculpts this base noise, and the influence of these shader-defined patterns began to yield compelling and controllable results in the generated outputs.
+
This hybrid methodologyโa delicate equilibrium between the familiar (the model\'s learned representations) and the novel (the intentionally introduced structure)โfosters both generative stability and exploratory capacity.
+
+
The Blend: A Cornerstone Principle
+
Respecting a model\'s training by blending shader-generated noise with the base noiseโas opposed to outright replacementโis a cornerstone of ShaderNoiseKSampler. This approach preserves generative coherence while enabling nuanced, controllable structural influence.
+
+
+ "Not to replace the chaos, but to give it direction
+ Not to eliminate randomness, but to make it purposeful
+ To transform noise into navigation, and navigation into art"
+ โ The Genesis Vision
+
+
+
+
+
๐บ๏ธ Exploring Latent Space: A Developing Idea
+
The exploration extended to the nature of latent space. It became apparent that by adjusting shader noise parameters with a consistent seed, one could systematically alter the generative output. This suggested a way to modify not just noise characteristics, but potentially the "topology" of the latent space being explored.
+
This led to an interesting mode of exploration. It was observed that different seeds seemed to contain unique thematic elementsโrecurring characters, color palettes, objects, and stylesโthat could be traced and explored. ShaderNoiseKSampler evolved from a noise tool into a means of navigating within a model's latent space with more specific control. This helped clarify the tool's potential.
+
Each parameter adjustment can be seen as a step along conceptual pathways, each blend mode a different way to perceive and traverse the possibilities.
+
+
+
+
+
+
+
+
+
๐ Core Concept โณ The Alchemical Transformation
+
ShaderNoiseKSampler implements the formula Lt=Sฮฑ(N)โKฮฒ(t)โa mathematical invocation where structured noise Sฮฑ transforms the base noise N before it enters the diffusion sampling process Kฮฒ. This elegant formula captures the essence of guided chaos, where randomness is structured according to sacred patterns before manifesting in the final image.
+
+
+
The Mathematics of Transformation
+
The core equation Lt=Sฮฑ(N)โKฮฒ(t) can be understood as follows:
+
+ Lt = Sฮฑ(N) โ Kฮฒ(t)
+
+
Where:
+
+
Lt: The final latent representation at timestep t
+
Sฮฑ(N): The shader transformation with parameters ฮฑ applied to base noise N
+
Kฮฒ(t): The diffusion sampling process with parameters ฮฒ at timestep t
+
โ: Function composition operator (application of one function to the result of another)
+
+
This mathematical formulation allows for more precise control over the noise structures that guide the generative process, creating a harmony between randomness and order.
+
+
+
+
โ๏ธ The Role of ShaderToTensor: Bridging Code and Concept
+
The ShaderToTensor class, implemented in shader_to_tensor.py, is the cornerstone of this system\'s ability to translate abstract shader concepts into concrete PyTorch tensors. It acts as the alchemical crucible where mathematical descriptions of noise are transmuted into the actual structured noise (Sฮฑ in our formula) that the KSampler can utilize.
+
Key functions of ShaderToTensor include:
+
+
Noise Synthesis: It contains the Python and PyTorch implementations of various noise algorithms (Perlin, Cellular, Curl, etc.). When you select a \'Shader Noise Type\' in the UI, you\'re choosing a specific method within this class.
+
Parameter Interpretation: It takes the parameters you set (Scale, Octaves, Warp Strength, etc.) and uses them to control the generation of these noise tensors.
+
Tensor Formatting: It ensures the generated noise is in the correct format (shape, data type, device) to be compatible with the ComfyUI latent workflow and the KSampler. This includes handling channel expansion (e.g., from 1-channel noise to 4 or 9 channels for the latent space).
+
Temporal Coherence Logic: For animated noise, ShaderToTensor (often in conjunction with specialized generator classes) implements the logic to produce evolving noise patterns over a time dimension.
+
+
Essentially, ShaderToTensor is the engine that powers the Sฮฑ(N) part of the equation. Without it, the conceptual shader patterns would remain abstract; this class makes them tangible and usable by the diffusion model.
+
+
+
+
๐ Essential Parameters
+
+
+
Shader Noise Type
+
The fundamental pattern archetype (tensor_field, curl_noise, domain_warp, etc.)
+
+
+
Noise Scale
+
Controls the frequency of pattern repetition (1.0 is the harmonic baseline)
+
+
+
Noise Octaves
+
The number of recursive self-similar layers (follows Fibonacci principles)
+
+
+
Noise Warp Strength
+
Controls the amount of displacement or distortion applied to the noise coordinates, altering the pattern's structure.
+
+
+
Noise Phase Shift
+
Adjusts the input to the noise function, effectively shifting the generated pattern. This can be used to explore variations of the noise.
+
+
+
Shape Mask Type
+
The geometric form overlaid upon the noise (radial, spiral, hexgrid, etc.)
+
+
+
Shape Mask Strength
+
Controls the intensity of the shape mask, determining how much of the noise is affected by the mask.
+
+
+
Noise Transformation
+
Mathematical operations (e.g., absolute, sin, sqrt) applied to the shader noise before blending. [Recommended: Experiment freely. See 'Noise Transformations' section for details].
+
+
+
Blend Mode
+
The alchemical method of combining shader noise with base noise (multiply, add, etc.). [Recommended: Multiply, Normal, Screen, Overlay].
+
+
+
Color Intensity
+
Adjusts the impact of the selected color scheme on the noise pattern.
+
+
+
Sequential Stages
+
Number of sequential shader stages to apply before injection stages. Each stage can have varied strength and apply noise over a specific portion of the diffusion steps. [Recommended: 1 - 3].
+
+
+
Injection Stages
+
Number of injection shader stages to apply after sequential stages. These stages typically inject noise at specific steps within the diffusion process, allowing for targeted interventions. [Recommended: 0 - 2].
+
+
+
Shader Strength
+
Overall strength of the shader noise influence for all stage types. Set to 0.0 to disable shader noise and use only the base noise. This acts as a global multiplier for stage-specific strengths. [Recommended: 0.1 - 0.4].
Number of sampling iterations. [Recommended: 20 - 60].
+
+
+
Sampler & Scheduler Compatibility
+
+
Sampler
+
Compatibility
+
Notes
+
+
Euler_Ancestral
+
High
+
Excellent across all model types
+
+
dpm_2_ancestral
+
High
+
Strong performance with all models
+
+
dpmppm_2_ancestral
+
High
+
Reliable for all model variants
+
+
LCM
+
High
+
Consistent results across models
+
+
Scheduler
+
Compatibility
+
Notes
+
+
Beta
+
High
+
Recommended scheduler
+
+
Normal
+
High
+
Good general performance
+
+
Simple
+
High
+
Effective for most cases
+
+
kl_optimal
+
High
+
Optimized performance
+
+
+
+
+
+
+
+
+
+
+
๐ต๏ธ Shader Noise Patterns โง The Twelve Noise Archetypes
+
ShaderNoiseKSampler harnesses twelve fundamental shader noise archetypes, each channeling a different aspect of mathematical reality. These patterns are the building blocks of visual coherence, from the flowing whorls of Perlin noise to the cosmic web-like structures of Tensor Fields. Each archetype generates its own distinct style of noise, offering a diverse palette for visual expression.
+
+
+
+
+
Tensor Field
+
Manifests flow patterns based on tensor mathematics, revealing the underlying force vectors of the mathematical space.
Controls the frequency spectrum directly, allowing precise frequency band manipulation.
+
+
+
+
+
3D Projection
+
Projects three-dimensional noise onto a 2D plane, creating depth and volumetric effects.
+
+
+
+
+
Curl Noise
+
Generates divergence-free vector fields that create perfect fluid-like flow patterns.
+
+
+
+
+
+
๐ Unlock Exclusive Shader Noise Palettes!
+
Become a valued member to access an expanded library of unique shader noise archetypes, advanced shader features, and custom ComfyUI tools. Your support helps fuel further development!
+
+
+
+
+
+
+
+
โ๏ธ Noise Mathematics ๐ The Underlying Formulations
+
Each shader noise archetype in ShaderNoiseKSampler is built upon rigorous mathematical foundations. Understanding these mathematical principles reveals the sacred geometry inherent in these patterns and allows for more intentional application in the creative process.
+
+
+
Perlin
+
Cellular
+
Tensor Field
+
Curl Noise
+
Domain Warp
+
Fractal
+
Waves
+
Gaussian
+
Heterogeneous FBM
+
Interference
+
Spectral
+
3D Projection
+
+
+
+
Perlin Noise Mathematics
+
Perlin noise, developed by Ken Perlin in 1983, uses a grid of random gradient vectors with interpolation to create smooth, natural-looking noise. The core computation involves:
+
+ n(x,y) = โ ฯi ยท gi ยท ((x,y) - (xi,yi))
+
+
Where gi are random gradient vectors at grid points, and ฯi are interpolation weights. The smoothstep function provides the basis for the interpolation:
+
+ smoothstep(t) = t2(3 - 2t)
+
+
This smooth interpolation creates the characteristic flowing appearance of Perlin noise, making it ideal for natural phenomena like terrain, clouds, and flowing water.
+
+
+ Perlin Noise Implementation
+
+
+
def perlin_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters for noise generation
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = shader_params.get("shaderOctaves", 3)
+ warp_strength = shader_params.get("shaderWarpStrength", 0.5)
+ phase_shift = shader_params.get("shaderPhaseShift", 0.5)
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Apply Perlin noise with multiple octaves
+ result = torch.zeros(batch_size, height, width, 1, device=device)
+ amplitude = 0.5
+ frequency = 1.0
+
+ for i in range(int(octaves)):
+ # Apply domain warping if warp_strength > 0
+ if i > 0 and warp_strength > 0.0:
+ warp = compute_warp_field(p, frequency, time, seed + i)
+ noise = sample_perlin(p * frequency + warp * warp_strength, seed + i)
+ else:
+ noise = sample_perlin(p * frequency, seed + i)
+
+ # Add to result with decreasing amplitude
+ result += amplitude * noise
+
+ # Prepare for next octave
+ frequency *= 2.0
+ amplitude *= 0.5
+
+ # Apply phase shift (contrast)
+ result *= (1.0 + phase_shift)
+
+ return result
+
+
+
+
+
Cellular Noise Mathematics
+
Cellular noise (also known as Worley noise) creates Voronoi-like patterns based on distance metrics to feature points:
Where dist() is a distance function (typically Euclidean) and Fn returns the distance to the nth closest feature point. Various cellular patterns emerge by combining these distances:
+
+ F2 - F1 : cell edges
+ F1 : cell interiors
+ 2F1 - F2 : cracks
+
+
These combinations create patterns reminiscent of cellular structures, bubbles, and organic tissues.
+
+
+ Cellular Noise Implementation
+
+
+
def cellular_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = shader_params.get("shaderOctaves", 1)
+ # warp_strength = shader_params.get("shaderWarpStrength", 0.5) # Not used in this snippet
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Apply cellular noise
+ result = torch.zeros(batch_size, height, width, 1, device=device)
+
+ # Generate cellular pattern based on octaves mode
+ pattern_type = int(octaves) % 4 # Use octaves to select pattern type
+
+ # Calculate F1 and F2 distances (assuming compute_feature_distances exists)
+ f1, f2 = compute_feature_distances(p * scale, seed)
+
+ # Select pattern based on type
+ if pattern_type == 0:
+ result = f1 # Basic Worley/cellular noise
+ elif pattern_type == 1:
+ result = f2 # Second closest point
+ elif pattern_type == 2:
+ result = f2 - f1 # Classic cellular look with ridges
+ else:
+ result = f1 * f2 # Different look with smaller features
+
+ return result
+
+
+
+
+
Tensor Field Mathematics
+
Tensor fields represent directional information at every point in space, creating flow-like patterns. The core mathematics involves computing the eigenvalues ฮป and eigenvectors v of tensor matrices:
In 2D, this simplifies to a scalar field where the curl is perpendicular to the plane:
+
+ curl(ฮจ)(x,y) = โฮจy/โx - โฮจx/โy
+
+
This mathematical property ensures the resulting vector field has zero divergence (โยทv = 0), creating perfect flow patterns without sources or sinks.
+
+
+ Curl Noise Implementation
+
+
+
def curl_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ # warp_strength = shader_params.get("shaderWarpStrength", 0.5) # Not used directly here for simple curl value
+
+ # Create coordinate grid
+ y_coords, x_coords = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ # p = torch.stack([x_coords, y_coords], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1) # Not used
+
+ # Compute potential fields (scalar fields to take curl of, assuming compute_noise_field)
+ # For 2D curl, we need two scalar potential fields or components of a vector potential field
+ # Let's assume ฮจ = (ฯx(x,y,t), ฯy(x,y,t)) and we want curl_z = d(ฯy)/dx - d(ฯx)/dy
+
+ # Simplified: generate two noise fields for potential components
+ # These would typically be generated by a noise function like Perlin or Simplex
+ potential_y_coords = torch.stack([x_coords * scale, y_coords * scale + time], dim=-1)
+ potential_x_coords = torch.stack([x_coords * scale + time, y_coords * scale], dim=-1)
+
+ # Placeholder for actual noise generation (e.g., simplex_noise(potential_coords, seed))
+ potential_y = torch.sin(potential_y_coords[...,0] * 5) * torch.cos(potential_y_coords[...,1] * 5)
+ potential_x = torch.cos(potential_x_coords[...,0] * 5) * torch.sin(potential_x_coords[...,1] * 5)
+
+ potential_y = potential_y.unsqueeze(0).unsqueeze(-1).repeat(batch_size,1,1,1)
+ potential_x = potential_x.unsqueeze(0).unsqueeze(-1).repeat(batch_size,1,1,1)
+
+
+ # Calculate curl using finite differences
+ epsilon = 0.01 # step for derivative
+
+ # Partial derivative of potential_y with respect to x
+ # potential_y(x + eps) - potential_y(x - eps) / (2*eps)
+ # We'd need to sample noise at shifted coords or use analytical derivatives if possible
+ # Simplified finite difference on the grid:
+ dpy_dx = (torch.roll(potential_y, shifts=-1, dims=2) - torch.roll(potential_y, shifts=1, dims=2)) / (2 * (2.0/width) ) # dx = 2.0/width for range -1 to 1
+
+ # Partial derivative of potential_x with respect to y
+ # potential_x(y + eps) - potential_x(y - eps) / (2*eps)
+ dpx_dy = (torch.roll(potential_x, shifts=-1, dims=1) - torch.roll(potential_x, shifts=1, dims=1)) / (2 * (2.0/height) ) # dy = 2.0/height
+
+ # Curl in 2D (z-component)
+ curl_z = dpy_dx - dpx_dy
+
+ return curl_z # This is a scalar field representing curl strength
+
+
+
+
+
+
Domain Warp Mathematics
+
Domain warping is a technique where the input coordinates (the domain) of a noise function are displaced or distorted by another noise function. This creates swirling, turbulent, or flowing effects in the final pattern. Instead of sampling noise at point P, we sample at P + offset(P), where offset(P) is itself a noise function.
Noisebase: The primary noise function (e.g., Perlin, Simplex).
+
Noisedisplacement: A noise function generating the offset vectors.
+
Swarp: Warp strength, controlling the magnitude of distortion.
+
Fwarp: Warp frequency, controlling the detail of the distortion.
+
+
By applying this recursively (warping the domain of the displacement noise itself), more complex fractal warping effects can be achieved.
+
+
+ Domain Warp Implementation
+
+
+
def domain_warp_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = shader_params.get("shaderOctaves", 3)) # Used for the base noise often
+ warp_strength = shader_params.get("shaderWarpStrength", 0.5)
+ warp_frequency_mult = shader_params.get("shaderWarpFrequency", 0.5) # Relative frequency for warp noise
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Time component for animation
+ t_offset = time * 0.1
+
+ # Generate displacement fields (two noise channels for 2D offset)
+ # It's common to use a simpler noise like Perlin for displacement
+ # Assuming a 'sample_perlin_octaves' function for simplicity
+
+ # Coordinates for displacement noise
+ p_warp = p * scale * warp_frequency_mult
+
+ # Simplified Perlin sampling for displacement
+ # In a real scenario, you'd use an actual Perlin/Simplex implementation
+ q_x = sample_perlin_like(p_warp + torch.tensor([10.3 + t_offset, 20.7], device=device), seed + 100)
+ q_y = sample_perlin_like(p_warp + torch.tensor([-5.1, -15.9 + t_offset], device=device), seed + 200)
+
+ displacement = torch.cat([q_x, q_y], dim=-1) * warp_strength
+
+ # Apply displacement to original coordinates
+ p_displaced = p * scale + displacement
+
+ # Generate base noise with displaced coordinates
+ # Again, assuming a base noise function (e.g., Perlin with octaves)
+ # result = sample_perlin_octaves(p_displaced, octaves, seed)
+ result = sample_perlin_like(p_displaced, seed) # Simplified
+
+ return result
+
+# Helper placeholder for a Perlin-like noise generation
+def sample_perlin_like(coords, seed):
+ # This is a highly simplified stand-in for actual Perlin noise
+ # A real implementation would involve gradients, lattice points, interpolation etc.
+ torch.manual_seed(seed)
+ # Create some pseudo-randomness based on coords and seed
+ # This is NOT Perlin noise but serves as a placeholder structure
+ noise = torch.sin(coords[..., 0] * 5.0 + coords[..., 1] * 3.0 + seed * 0.1) * \
+ torch.cos(coords[..., 0] * 2.0 - coords[..., 1] * 6.0 + seed * 0.05)
+ return noise.unsqueeze(-1) # Add channel dimension
+
+
+
+
+
+
Fractal Noise (FBM) Mathematics
+
Fractal noise, often implemented as Fractal Brownian Motion (FBM), is a fundamental technique for generating natural-looking textures. It is constructed by summing multiple layers (octaves) of a base noise function (like Perlin or Simplex). Each successive octave has a higher frequency and a lower amplitude.
+
+ FBM(P) = โN-1i=0 Ai ร Noise(Fi ร P)
+
+
Where:
+
+
P: The input coordinate.
+
N: The number of octaves (layers of detail).
+
Noise: The base coherent noise function (e.g., Perlin).
+
A: Amplitude factor (persistence, typically < 1, e.g., 0.5). Controls how much amplitude decreases per octave.
+
F: Frequency factor (lacunarity, typically > 1, e.g., 2.0). Controls how much frequency increases per octave.
+
+
The summation creates a pattern that exhibits self-similarity across different scales, characteristic of many natural phenomena.
+
+
+ Fractal Noise (FBM) Implementation
+
+
+
def fractal_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = int(shader_params.get("shaderOctaves", 4))
+ persistence = shader_params.get("shaderPersistence", 0.5) # Amplitude scaling
+ lacunarity = shader_params.get("shaderLacunarity", 2.0) # Frequency scaling
+ phase_shift = shader_params.get("shaderPhaseShift", 0.0) # For adding variation
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ total_noise = torch.zeros(batch_size, height, width, 1, device=device)
+ current_amplitude = 1.0
+ current_frequency = 1.0
+ normalization_factor = 0.0
+
+ for i in range(octaves):
+ # Apply scale and current frequency to coordinates
+ # Add time and phase_shift for variation / animation
+ coords_at_octave = p * scale * current_frequency + time * 0.05 * current_frequency + phase_shift * i
+
+ # Generate base noise (e.g., Perlin or Simplex)
+ # Assuming 'sample_perlin_like' as a placeholder for a proper noise function
+ noise_val = sample_perlin_like(coords_at_octave, seed + i)
+
+ total_noise += noise_val * current_amplitude
+ normalization_factor += current_amplitude
+
+ current_amplitude *= persistence
+ current_frequency *= lacunarity
+
+ # Normalize the result to roughly [-1, 1] range
+ if normalization_factor > 0:
+ total_noise /= normalization_factor
+
+ return total_noise
+
+
+
+
+
+
Waves Noise Mathematics
+
Waves noise is typically generated by summing multiple sine or cosine wave functions. Each wave can have its own amplitude, frequency, phase, and direction. The superposition of these waves can create a wide variety_of_patterns, from simple ripples to complex interference effects.
+
+ Waves(P) = โi Ai ร sin(ki โ P + ฯit + ฯi)
+
+
Where for each wave i:
+
+
P: The input coordinate (e.g., (x,y)).
+
Ai: Amplitude of the wave.
+
ki: Wave vector (determines direction and spatial frequency/wavelength). Its magnitude |ki| = 2ฯ / ฮปi.
+
ฯi: Angular frequency (determines temporal oscillation speed).
+
t: Time.
+
ฯi: Phase offset.
+
โ : Dot product.
+
+
Simpler forms might fix directions or use scalar frequencies if directionality is not complex.
+
+
+ Waves Noise Implementation
+
+
+
def waves_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0) # General scale for coordinates
+ num_waves = int(shader_params.get("shaderOctaves", 3)) # Use 'octaves' as number of waves
+ base_frequency = shader_params.get("shaderBaseFrequency", 5.0)
+ amplitude_variation = shader_params.get("shaderAmplitudeVariation", 0.5)
+ phase_shift_speed = shader_params.get("shaderPhaseShift", 0.2) # Use for time-based phase shift
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+ p = p * scale # Apply general scale
+
+ total_waves = torch.zeros(batch_size, height, width, 1, device=device)
+
+ torch.manual_seed(seed) # For reproducible wave directions/phases
+
+ for i in range(num_waves):
+ # Generate random direction for each wave
+ angle = torch.rand(1, device=device) * 2.0 * 3.14159265 # math.pi
+ direction = torch.tensor([torch.cos(angle), torch.sin(angle)], device=device).view(1, 1, 1, 2)
+
+ frequency = base_frequency * (1.0 + (torch.rand(1, device=device) - 0.5) * 0.5 * i) # Vary frequency slightly
+ amplitude = 1.0 / num_waves * (1.0 + (torch.rand(1, device=device) - 0.5) * amplitude_variation)
+ phase = torch.rand(1, device=device) * 2.0 * 3.14159265 + time * phase_shift_speed * (i + 1)
+
+ # Project coordinates onto wave direction: dot(p, direction)
+ wave_input = (p * direction).sum(dim=-1, keepdim=True)
+
+ wave_val = torch.sin(wave_input * frequency + phase) * amplitude
+ total_waves += wave_val
+
+ # Normalize to be roughly in [-1, 1], though sum of sines can exceed this
+ # A more robust normalization might be needed depending on num_waves and amplitudes
+ # total_waves = torch.clamp(total_waves, -1.0, 1.0)
+
+ return total_waves
+
+
+
+
+
+
Gaussian Noise Mathematics
+
Gaussian noise is a statistical noise characterized by a probability density function (PDF) that follows the Gaussian (or normal) distribution. Its values are typically clustered around a mean (ฮผ), with a spread determined by the standard deviation (ฯ). In image processing, it's often used to simulate random sensor noise or as a basis for other effects.
For generating Gaussian noise, random numbers are drawn from this distribution. In practice, libraries provide functions to directly generate such noise (e.g., torch.randn which samples from N(0,1)).
+
+
ฮผ (Mean): The average value, often 0 for noise.
+
ฯ (Standard Deviation): Controls the "spread" or intensity of the noise. ฯ2 is the variance.
+
+
Gaussian noise is "white" if its values are statistically independent and identically distributed at each point.
+
+
+ Gaussian Noise Implementation
+
+
+
def gaussian_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ mean = shader_params.get("shaderMean", 0.0)
+ std_dev = shader_params.get("shaderStdDev", 1.0) # This is 'scale' in some contexts
+ # 'scale' from shader_params usually means spatial scale for other noises,
+ # for Gaussian, it often refers to standard deviation or amplitude.
+ # Let's use a specific param like shaderStdDev.
+
+ # Ensure seed is used for reproducibility
+ torch.manual_seed(seed)
+
+ # Generate Gaussian noise
+ # torch.randn generates noise from a standard normal distribution (mean 0, std 1)
+ noise_tensor = torch.randn(batch_size, 1, height, width, device=device)
+
+ # Adjust mean and standard deviation
+ # Noise = Mean + StdDev * StandardNormalNoise
+ noise_tensor = mean + std_dev * noise_tensor
+
+ # Unlike coherent noise (Perlin, etc.), Gaussian noise typically doesn't use a coordinate grid
+ # for its generation, as each pixel's value is independent (for white Gaussian noise).
+ # Parameters like 'scale', 'octaves' are less relevant in the typical sense.
+ # 'time' could be used to modulate mean or std_dev for animated effects if desired.
+ # For example: animated_std_dev = std_dev * (1.0 + 0.5 * torch.sin(torch.tensor(time)))
+ # noise_tensor = mean + animated_std_dev * noise_tensor
+
+ # Output is typically 1 channel, but can be expanded if needed for coloring or blending.
+ # The KSampler expects 4 channels if blending with latent.
+ # This function would typically output raw noise, channel expansion happens later.
+
+ return noise_tensor # Shape: [batch_size, 1, height, width]
+
+
+
+
+
+
Heterogeneous FBM Mathematics
+
Heterogeneous Fractal Brownian Motion (Hetero FBM) is an extension of standard FBM. While standard FBM uses constant parameters (like persistence/amplitude falloff and lacunarity/frequency gain) across all octaves and spatial locations, Hetero FBM allows these parameters, or the base noise characteristics, to vary spatially. This creates textures with non-uniform complexity, where some areas might be smoother and others rougher or more detailed.
A(P, i): Spatially varying amplitude for octave i at point P.
+
F(P, i): Spatially varying frequency for octave i at point P.
+
Noise(..., params(P,i)): Base noise function whose own internal parameters might also vary spatially.
+
+
The spatial variation itself is often controlled by another noise function or a predefined map. For example, the Hurst exponent (H), which relates to persistence, could be made to vary across the domain.
+
+
+ Heterogeneous FBM Implementation
+
+
+
def heterogeneous_fbm_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract base parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = int(shader_params.get("shaderOctaves", 4))
+
+ # Parameters for heterogeneity - these could be controlled by other noise fields
+ base_persistence = shader_params.get("shaderPersistence", 0.5)
+ base_lacunarity = shader_params.get("shaderLacunarity", 2.0)
+ heterogeneity_strength = shader_params.get("shaderHeteroStrength", 0.3)
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Generate a control noise field for varying persistence/lacunarity
+ # This control noise determines how parameters change spatially
+ control_coords = p * scale * 0.3 + time * 0.02 # Slower varying control
+ control_noise = sample_perlin_like(control_coords, seed + 500) # Placeholder
+
+ total_noise = torch.zeros(batch_size, height, width, 1, device=device)
+ current_amplitude = torch.ones(batch_size, height, width, 1, device=device)
+ current_frequency = torch.ones(batch_size, height, width, 1, device=device) * scale
+ normalization_factor = torch.zeros(batch_size, height, width, 1, device=device)
+
+ for i in range(octaves):
+ # Modulate persistence and lacunarity spatially using control_noise
+ # This is a simplified modulation
+ spatial_persistence_mod = (control_noise * heterogeneity_strength) # Range e.g. [-0.3, 0.3]
+ spatial_lacunarity_mod = (control_noise * heterogeneity_strength * 0.5)
+
+ current_persistence = torch.clamp(base_persistence + spatial_persistence_mod, 0.1, 0.9)
+ current_lacunarity = torch.clamp(base_lacunarity + spatial_lacunarity_mod, 1.1, 3.0)
+
+ coords_at_octave = p * current_frequency + time * 0.05 * current_frequency.mean() # Use mean freq for time anim
+
+ noise_val = sample_perlin_like(coords_at_octave, seed + i)
+
+ total_noise += noise_val * current_amplitude
+ normalization_factor += current_amplitude
+
+ current_amplitude *= current_persistence
+ current_frequency *= current_lacunarity
+
+ if normalization_factor.min() > 1e-5: # Avoid division by zero
+ total_noise /= normalization_factor
+ else: # Fallback if normalization factor is too small everywhere
+ total_noise = torch.clamp(total_noise, -1.0, 1.0)
+
+ return total_noise
+
+
+
+
+
+
Interference Noise Mathematics
+
Interference patterns arise from the superposition of two or more waves. When waves meet, they can reinforce each other (constructive interference) or cancel each other out (destructive interference), depending on their relative phases and amplitudes. This principle is fundamental in physics (e.g., light and sound waves) and can be used to generate complex visual patterns.
A common way to generate interference is by summing or multiplying wave functions:
+
Example with two sine waves:
+
+ V(P) = A1sin(k1โ P + ฯ1) + A2sin(k2โ P + ฯ2)
+
+
Or using noise functions as sources:
+
+ V(P) = Noise1(P) + Noise2(P ร S + O)
+
+
Where S is a scale and O is an offset for Noise2 to make it different from Noise1. The visual character depends heavily on the frequencies and relative phases of the interfering sources.
+
+
+ Interference Noise Implementation
+
+
+
def interference_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale1 = shader_params.get("shaderScale", 1.0)
+ scale2 = shader_params.get("shaderScale2", 1.5) # Scale for the second noise source
+ octaves1 = int(shader_params.get("shaderOctaves", 3))
+ octaves2 = int(shader_params.get("shaderOctaves2", 2))
+ phase_offset = shader_params.get("shaderPhaseShift", 0.5) # Controls relative phase or timing
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Generate two separate noise fields
+ # Using placeholder 'sample_fbm_like' for simplicity
+ noise_field1_coords = p * scale1 + time * 0.03
+ noise_field1 = sample_fbm_like(noise_field1_coords, octaves1, seed, persistence=0.5, lacunarity=2.0)
+
+ noise_field2_coords = p * scale2 + time * 0.05 + phase_offset # Add phase_offset to vary interaction
+ noise_field2 = sample_fbm_like(noise_field2_coords, octaves2, seed + 10, persistence=0.6, lacunarity=1.8)
+
+ # Combine the noise fields to create interference
+ # Different combination methods yield different patterns:
+ # 1. Addition: result = noise_field1 + noise_field2
+ # 2. Multiplication: result = noise_field1 * noise_field2
+ # 3. Difference: result = noise_field1 - noise_field2
+ # 4. Using trigonometric functions: result = torch.cos(noise_field1 * math.pi) + torch.sin(noise_field2 * math.pi)
+
+ # Example: Cosine/Sine interference for more distinct patterns
+ pi = 3.14159265
+ interference_pattern = torch.cos(noise_field1 * pi) + torch.sin(noise_field2 * pi * (1.0 + phase_offset*0.2))
+
+ # Normalize the result (sum of two sines/cosines can range from -2 to 2)
+ result = interference_pattern / 2.0
+ result = torch.clamp(result, -1.0, 1.0)
+
+ return result
+
+# Placeholder for FBM-like noise generation
+def sample_fbm_like(coords, octaves, seed, persistence, lacunarity):
+ total_noise = torch.zeros_like(coords[..., 0:1]) # Ensure output has 1 channel
+ current_amplitude = 1.0
+ current_frequency = 1.0
+ temp_coords = coords # Avoid modifying original coords if it's used elsewhere
+
+ for i in range(int(octaves)):
+ noise_val = sample_perlin_like(temp_coords * current_frequency, seed + i)
+ total_noise += noise_val * current_amplitude
+ current_amplitude *= persistence
+ current_frequency *= lacunarity
+
+ # Basic normalization attempt
+ max_possible_amp = sum([persistence**i for i in range(int(octaves))])
+ if max_possible_amp > 0:
+ total_noise /= max_possible_amp
+
+ return total_noise
+
+
+
+
+
+
Spectral Noise Mathematics
+
Spectral noise generation involves directly defining or manipulating the noise's properties in the frequency domain (its spectrum) using techniques like the Fourier Transform. By controlling the amplitude and phase of different frequencies, a wide variety_of_textures can be created, from smooth to rough, or with specific directional biases.
+
The general process is:
+
+
Start with white noise in the spatial domain or directly create a spectrum.
+
Compute its Fast Fourier Transform (FFT) to get the frequency domain representation (spectrum).
+
Modify the spectrum: Apply a filter (e.g., 1/fฮฒ for pink/brown/blue noise, band-pass, directional filters).
+
Compute the Inverse Fast Fourier Transform (IFFT) to convert the modified spectrum back to the spatial domain.
The filter often takes the form 1/fฮฒ, where f is frequency and ฮฒ controls the "color" of the noise (e.g., ฮฒ=0 for white, ฮฒ=1 for pink, ฮฒ=2 for brown/red).
+
+
+ Spectral Noise Implementation
+
+
+
def spectral_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ beta = shader_params.get("shaderSpectralBeta", 1.0) # Exponent for 1/f^beta filter
+ # A low-pass/high-pass cutoff might also be a parameter
+ low_freq_boost = shader_params.get("shaderLowFreqBoost", 1.0)
+ high_freq_attenuation = shader_params.get("shaderHighFreqAtten", 1.0)
+
+ torch.manual_seed(seed)
+
+ # 1. Start with white noise in spatial domain
+ white_noise_spatial = torch.rand(batch_size, 1, height, width, device=device) * 2.0 - 1.0
+
+ # 2. Compute FFT
+ # torch.fft.fft2 performs 2D FFT. For real input, rfft2 is more efficient.
+ spectrum = torch.fft.rfft2(white_noise_spatial, norm="ortho") # norm="ortho" is often good
+
+ # Create frequency coordinates (kx, ky)
+ # Frequencies for rfft2: ky goes from 0 to H//2. kx goes from 0 to W-1, then wraps around.
+ ky = torch.fft.rfftfreq(height, d=1.0/height, device=device) # Normalized frequencies
+ kx = torch.fft.fftfreq(width, d=1.0/width, device=device) # Frequencies for full FFT
+
+ # For rfft2, kx used will be kx[:W//2 + 1]
+ kx_r = kx[:width//2 + 1]
+
+ # Create a 2D grid of frequencies (squared magnitude for filter)
+ # Broadcasting ky.unsqueeze(1) and kx_r.unsqueeze(0)
+ freq_sq = ky.unsqueeze(1)**2 + kx_r.unsqueeze(0)**2
+ freq_sq = freq_sq.unsqueeze(0).unsqueeze(0) # Add batch and channel dims
+
+ # Avoid division by zero at DC component (freq=0)
+ freq_sq[freq_sq < 1e-6] = 1e-6
+
+ # 3. Create and apply the 1/f^beta filter
+ # Power spectrum is proportional to 1 / (freq_magnitude ^ beta)
+ # Amplitude is proportional to 1 / (freq_magnitude ^ (beta/2))
+ filter_amplitude = (1.0 / (torch.sqrt(freq_sq)**(beta/2.0)))
+
+ # Apply additional boosts/attenuations
+ # Example: Boost low frequencies, attenuate high frequencies
+ # This is a very simplified frequency band control
+ freq_magnitude = torch.sqrt(freq_sq)
+ filter_amplitude *= torch.exp(- (freq_magnitude / (height*0.5)) * (1.0-low_freq_boost) ) # Boost low (smaller effect here)
+ filter_amplitude *= torch.exp(- (freq_magnitude / (height*0.1)) * (high_freq_attenuation-1.0) ) # Attenuate high
+
+ # Ensure DC component (mean) is not overly amplified if beta is high
+ # This depends on specific needs. Sometimes DC is zeroed out or handled separately.
+ # filter_amplitude[..., 0, 0] = 1.0 # Or some other controlled value
+
+ modified_spectrum = spectrum * filter_amplitude
+
+ # Add some phase shift based on time for animation (subtle effect)
+ if time > 0:
+ phase_shift_val = torch.exp(1j * freq_magnitude * time * 0.01)
+ modified_spectrum *= phase_shift_val
+
+ # 4. Compute IFFT
+ spatial_noise_filtered = torch.fft.irfft2(modified_spectrum, s=(height, width), norm="ortho")
+
+ # Normalize the result to roughly [-1, 1]
+ std_val = torch.std(spatial_noise_filtered)
+ if std_val > 1e-5:
+ spatial_noise_filtered = (spatial_noise_filtered - torch.mean(spatial_noise_filtered)) / std_val
+
+ return torch.clamp(spatial_noise_filtered, -1.0, 1.0)
+
+
+
+
+
+
3D Projection Noise Mathematics
+
3D Projection noise involves generating a 3D noise field (e.g., 3D Perlin, Simplex, or FBM) and then sampling a 2D slice from it. This technique creates 2D patterns that appear to have depth, volume, or temporal evolution if the slice position changes over time.
+
+ Noise2D(x, y) = Noise3D(x ร Sxy, y ร Sxy, zslice ร Sz + T ร Vt)
+
+
Where:
+
+
(x, y): 2D coordinates for the output noise.
+
Noise3D: A 3D coherent noise function.
+
Sxy: Spatial scaling factor for x and y axes.
+
zslice: The depth or position of the 2D slice along the third dimension (often z).
+
Sz: Scaling factor for the z-dimension, controlling "thickness" or detail along z.
+
T: Time parameter for animation.
+
Vt: Velocity of slicing through the 3D noise field over time.
+
+
By animating zslice or rotating the sampling plane, dynamic evolving textures can be created from a static 3D noise volume.
+
+
+ 3D Projection Noise Implementation
+
+
+
def projection_3d_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale_xy = shader_params.get("shaderScale", 1.0) # Spatial scale for X and Y
+ scale_z = shader_params.get("shaderZScale", 1.0) # Scale for the Z dimension of the 3D noise
+ octaves = int(shader_params.get("shaderOctaves", 3))
+ z_slice_pos = shader_params.get("shaderZSlice", 0.0) # Static Z position for the slice
+ time_travel_speed = shader_params.get("shaderTimeSpeed", 0.1) # How fast 'time' moves the slice in Z
+
+ # Create 2D coordinate grid for x, y
+ y_coords, x_coords = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Prepare 3D coordinates for sampling 3D noise
+ # X and Y are scaled by scale_xy
+ # Z is determined by z_slice_pos, animated by time, and scaled by scale_z
+
+ x_3d = x_coords.unsqueeze(0).repeat(batch_size, 1, 1) * scale_xy
+ y_3d = y_coords.unsqueeze(0).repeat(batch_size, 1, 1) * scale_xy
+
+ # Calculate Z coordinate for the slice
+ # Static part from z_slice_pos, dynamic part from time
+ z_val_static = z_slice_pos
+ z_val_dynamic = time * time_travel_speed
+ z_3d_scalar = (z_val_static + z_val_dynamic) * scale_z
+
+ # Expand z_scalar to match dimensions of x_3d, y_3d for stacking
+ z_3d = torch.full_like(x_3d, z_3d_scalar)
+
+ # Stack to form [Batch, Height, Width, 3] coordinates
+ p_3d = torch.stack([x_3d, y_3d, z_3d], dim=-1)
+
+ # Generate 3D noise (e.g., 3D Perlin or Simplex FBM)
+ # Assuming a placeholder 'sample_3d_fbm_like'
+ # This function would take 3D coordinates and produce a scalar noise value.
+ result_3d_noise = sample_3d_fbm_like(p_3d, octaves, seed, persistence=0.5, lacunarity=2.0)
+
+ return result_3d_noise # Expected shape: [Batch, Height, Width, 1]
+
+# Placeholder for 3D FBM-like noise generation
+def sample_3d_fbm_like(coords_3d, octaves, seed, persistence, lacunarity):
+ # coords_3d shape: [B, H, W, 3]
+ total_noise = torch.zeros_like(coords_3d[..., 0:1]) # Output [B,H,W,1]
+ current_amplitude = 1.0
+ current_frequency = 1.0
+
+ for i in range(int(octaves)):
+ # In a real 3D noise, this would use a 3D hash/gradient function
+ # Simplified pseudo-3D noise for placeholder:
+ temp_coords = coords_3d * current_frequency
+ noise_val = (torch.sin(temp_coords[..., 0] + temp_coords[..., 2] * 0.5 + seed * 0.1 * (i+1)) *
+ torch.cos(temp_coords[..., 1] - temp_coords[..., 2] * 0.3 + seed * 0.05 * (i+1)))
+ noise_val = noise_val.unsqueeze(-1) # Add channel dim
+
+ total_noise += noise_val * current_amplitude
+ current_amplitude *= persistence
+ current_frequency *= lacunarity
+
+ max_possible_amp = sum([persistence**i for i in range(int(octaves))])
+ if max_possible_amp > 0:
+ total_noise /= max_possible_amp
+
+ return torch.clamp(total_noise, -1.0, 1.0)
+
+
+
+
+
+
+
+
+
+
๐ญ Blend Modes โ๏ธ The Alchemical Combinations
+
ShaderNoiseKSampler offers various blend modes to combine shader noise with base noise, each creating distinct alchemical transformations:
+
+
+
๐งฉ Blend Operations
+
+
+
Normal
+
Simple linear interpolation between base and shader noise, controlled by shader_strength
+
result = base * (1.0 - ฮฑ) + shader * ฮฑ
+
+
+
Add
+
Adds shader noise to base noise, creating brightened areas where patterns align
+
result = base + shader * ฮฑ
+
+
+
Multiply
+
Multiplies base and shader noise, darkening the overall pattern
+
result = base * (shader * ฮฑ + (1.0 - ฮฑ))
+
+
+
Screen
+
Inverts, multiplies, then inverts again, brightening patterns
+
result = 1.0 - (1.0 - base) * (1.0 - shader * ฮฑ)
+
+
+
Overlay
+
Combines Multiply and Screen modes for enhanced contrast
+
result = base < 0.5 ? 2.0 * base * shader : 1.0 - 2.0 * (1.0 - base) * (1.0 - shader)
+
+
+
Soft Light
+
Darkens or lightens colors depending on shader noise
+
result = (1.0 - base) * base * shader + base * (1.0 - (1.0 - base) * (1.0 - shader))
+
+
+
Hard Light
+
More intense version of Overlay with sharper contrast
Subtracts darker color from lighter color, creating distinctive edges
+
result = base * (1.0 - ฮฑ) + abs(base - shader) * ฮฑ
+
+
+
+
+
+
๐งฟ Blend Mode Harmonics
+
The choice of blend mode significantly impacts the generated image. Different blend modes resonate with different shader noise types and prompt themes:
Beyond the core shader noise patterns, ShaderNoiseKSampler offers additional mathematical transformations that can dramatically alter the character of the generated shader noise:
+
+
+
๐งฎ Mathematical Operators
+
+
+
Reverse
+
Inverts the sign of the noise, turning peaks into valleys and vice versa
+
T(noise) = -noise
+
+
+
Inverse
+
Reverses the values within the 0-1 range, preserving the overall pattern
+
T(noise) = 1.0 - noise
+
+
+
Absolute
+
Takes the absolute value, creating sharp ridges at zero-crossings
+
T(noise) = |noise|
+
+
+
Square
+
Squares the values, enhancing high values and diminishing low values
+
T(noise) = noise2
+
+
+
Square Root
+
Takes the square root, enhancing low values and compressing high values
+
T(noise) = โnoise
+
+
+
Log
+
Takes the logarithm, greatly enhancing low values
+
T(noise) = log(noise + ฮต)
+
+
+
Sin
+
Applies the sine function, creating oscillating patterns
+
T(noise) = sin(noise ร ฯ)
+
+
+
Cos
+
Applies the cosine function, creating alternating bands
+
T(noise) = cos(noise ร ฯ)
+
+
+
+
+
+
๐งช Transformation Alchemy
+
These transformations can dramatically alter the character of shader noise patterns, creating new structural properties:
+
+
Absolute transform creates ridge-like features along zero-crossings, perfect for geological formations
+
Square transform enhances high-intensity regions while subduing low-intensity areas, ideal for creating focused areas of interest
+
Sin/Cos transforms create banded patterns, excellent for stratified or layered structures
These transformations can be combined with different shader noise types and blend modes to create an almost infinite variety of structural guidance patterns.
+
+
+
+
+
+
+
๐ฌ Temporal Coherence โณ Evolving Noise for Animations
+
ShaderNoiseKSampler's temporal coherence feature ensures consistent noise patterns. For sequences like video frames, it helps maintain frame-to-frame consistency by evolving noise naturally over time. For single image generations, it ensures the base noise is derived consistently from the main seed, leading to predictable noise structures when parameters are tweaked.
+
+
+
โ๏ธ Enabling Evolving Animations
+
Temporal coherence fundamentally changes how shader noise is generated:
+
+
use_temporal_coherence (Boolean): This is the primary toggle. When enabled, ShaderNoiseKSampler ensures noise is generated consistently. For videos, it uses techniques (like the TemporalCoherentNoiseGenerator) that treat time as an evolving dimension. For images, it ensures the same base noise is used if the main seed is unchanged.
+
Consistent Base Seed: For temporal coherence to be effective (for both images and videos), the main seed input must remain constant. For videos, variation then comes from the noise evolving over an internal 'time' parameter. For images, this ensures that if other shader parameters are changed, the underlying base noise structure remains the same.
+
Consistent/Evolving Patterns: For videos, this mode generates patterns that transform coherently from one frame to the next, leading to more stable animations. For single images, it means the generated noise pattern will be the same for a given seed, even if other parameters (like shader strength or blend mode) are changed, allowing for more predictable exploration.
+
+
+
Conceptual animation preview placeholder.
+
+
+
+
+
+ Conceptual Temporal Noise (Python)
+
+
+
+import torch
+
+# Placeholder for a 3D simplex noise function
+def simplex_noise_3d_placeholder(coords_xyt, seed):
+ # coords_xyt is expected to be [B, H, W, 3] (x, y, time)
+ torch.manual_seed(seed)
+ noise = torch.sin(coords_xyt[..., 0] * 2.0 + coords_xyt[..., 2] * 1.5) * \
+ torch.cos(coords_xyt[..., 1] * 2.0 - coords_xyt[..., 2] * 1.0)
+ return noise.unsqueeze(-1) # Output [B, H, W, 1]
+
+def generate_animated_noise_frame(batch_size, height, width, scale, current_time, device, base_seed):
+ y_coords, x_coords = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ spatial_coords = torch.stack([x_coords, y_coords], dim=-1) * scale
+ time_coord_val = torch.full((height, width, 1), current_time, device=device)
+ p_temporal = torch.cat([spatial_coords, time_coord_val], dim=-1)
+ p_temporal_batch = p_temporal.unsqueeze(0).repeat(batch_size, 1, 1, 1)
+ frame_noise = simplex_noise_3d_placeholder(p_temporal_batch, base_seed)
+ return frame_noise.permute(0, 3, 1, 2)
+
+# --- Example Conceptual Usage for an Animation ---
+# total_frames = 100; animation_seed = 12345
+# for frame_idx in range(total_frames):
+# time_for_frame = frame_idx / total_frames
+# noise = generate_animated_noise_frame(1, 64, 64, 1.0, time_for_frame, 'cpu', animation_seed)
+ # noise would then be used by KSampler for this frame.
+
+
+
+
+
+
+
+
๐ญ Shape Masks ๐ Geometric Modulation
+
Shape masks provide spatial control by modulating shader noise patterns with procedurally generated geometric overlays. These masks, typically grayscale tensors (0.0 for no effect, 1.0 for full effect), guide the generative process, influencing compositions and textural details.
+
+
+
Mask Generation & Application
+
Shape masks are procedurally generated within the node as fixed grayscale patterns where values range from 0.0 (no effect area) to 1.0 (full effect area). The generated mask \( M \) is then used to modulate an input field (e.g., a noise or velocity field \( V \)) based on the shape_mask_strength \( \\alpha_s \). The application is a linear interpolation:
+
+ V_{masked} = V \\times (1 - \\alpha_s) + (V \\times M) \\times \\alpha_s
+
+
This means if \( \\alpha_s = 1 \), the field is fully multiplied by the mask pattern (\( V \\times M \)). If \( \\alpha_s = 0 \), the mask has no effect. Intermediate values blend between the original and the mask-modulated field. Key characteristics include:
+
+
Mask Type: Defines the base geometry from a selection including Radial, Linear, and various Geometric or Procedural patterns.
+
Mask Strength: The \( \\alpha_s \) parameter, controlling the blend intensity of the mask\'s effect.
+
Edge Handling: Mask types inherently produce different edge styles, some with soft, feathered falloffs (often using smoothstep internally) and others with hard, thresholded edges.
+
+
Note that while masks might appear animated in visualization previews, in the backend implementation they are static patterns applied at generation time. Any animation effects in the final output would come from changing parameters between frames.
import torch
+
+def apply_shape_mask_illustrative(input_field, coords_bhwc, mask_type="radial", shape_mask_strength=0.7):
+ '''Illustrative: Generate and apply a shape mask to an input field.
+ Args:
+ input_field (torch.Tensor): Field to be masked [B, H, W, C].
+ coords_bhwc (torch.Tensor): Coordinates for mask generation (0-1 range) [B, H, W, 2].
+ mask_type (str): Type of mask to generate.
+ shape_mask_strength (float): Strength of the mask application.
+ Returns:
+ torch.Tensor: Masked input_field.
+ '''
+ batch_size, height, width, _ = coords_bhwc.shape
+ device = coords_bhwc.device
+
+ # --- Mask Generation (Simplified Radial Example) ---
+ # Initialize shape_mask with ones (no effect by default if mask_type is unrecognized)
+ shape_mask = torch.ones_like(coords_bhwc[..., 0])
+
+ if mask_type == "radial":
+ center_x, center_y = 0.5, 0.5
+
+ # Calculate distance from the center
+ y_diff = coords_bhwc[..., 1] - center_y
+ x_diff = coords_bhwc[..., 0] - center_x
+ # Normalized distance, similar to GLSL masks (stretching to fill 0-1 range)
+ dist = torch.sqrt(x_diff**2 + y_diff**2) * 2.0
+ current_mask_values = torch.clamp(1.0 - dist, 0.0, 1.0)
+ shape_mask = current_mask_values
+ # ... (other mask_type implementations would go here)
+
+ # Ensure mask is [B, H, W, 1] for broadcasting with input_field [B, H, W, C]
+ shape_mask = shape_mask.unsqueeze(-1)
+
+ # --- Mask Application ---
+ # Modulate the input field with the generated mask pattern
+ field_modulated_by_mask_pattern = input_field * shape_mask
+
+ # Apply shape_mask_strength via linear interpolation (lerp)
+ # output_field = base_value + (target_value - base_value) * weight
+ # output_field = input_field + (field_modulated_by_mask_pattern - input_field) * shape_mask_strength
+ # This is equivalent to PyTorch's torch.lerp:
+ output_field = torch.lerp(input_field, field_modulated_by_mask_pattern, shape_mask_strength)
+
+ return output_field
+
+# --- Example Conceptual Usage ---
+# B, H, W, C_noise = 1, 64, 64, 2 # Example: 2-channel velocity field
+# example_field = torch.rand(B, H, W, C_noise, device='cpu') # Random field
+
+# # Create normalized coordinates (0 to 1 range)
+# y_coords_tensor = torch.linspace(0, 1, H, device='cpu').view(1, H, 1, 1).expand(B, H, W, 1)
+# x_coords_tensor = torch.linspace(0, 1, W, device='cpu').view(1, 1, W, 1).expand(B, H, W, 1)
+# # Coordinates are expected as [x, y] in the last dimension for mask generation
+# coordinates_tensor = torch.cat((x_coords_tensor, y_coords_tensor), dim=-1)
+
+# masked_field = apply_shape_mask_illustrative(example_field, coordinates_tensor, mask_type="radial", shape_mask_strength=0.8)
+# print(f"Masked field tensor shape: {masked_field.shape}")
+
+
+
+
+
+
๐จ Creative Applications
+
The interplay between different mask types, the noise they modulate, and various parameter settings (like shape_mask_strength, noise_scale, etc.) allows for a wide range of artistic effects. Consider these combinations:
+
+
Vignettes & Focus: Soft Radial or Vignette masks to draw attention centrally.
+
Structured Organics: Grid Masks combined with Cellular Noise for bio-mechanical effects.
+
Atmospheric Depth: Linear Gradient Masks to simulate fog or distance with flowing noise.
+
Selective Detailing: Geometric Masks to apply high-frequency noise to specific regions.
+
+
Experiment with mask types and their strengths in conjunction with noise parameters to guide the generative process.
+
+
+
+
+
+
+
๐ Color Schemes โ๏ธ Chromatic Adjustments
+
ShaderNoiseKSampler allows for the application of color transformations directly to the generated shader noise patterns. It's important to understand that this is not a traditional post-processing color adjustment. Instead, the color information is integrated into the shader noise before it influences the diffusion model during the sampling process. Consequently, these 'colors' can function as an integral part of the shader noise's texture and structure, subtly guiding the model's interpretation and shaping the features, patterns, and overall aesthetic of the final image. This technique offers a unique way to enhance visual impact and steer the generative outcome.
+
+
+
Color Transformation Process
+
The color mapping function transforms a normalized noise value t โ [0,1] into RGB color space through gradient functions. Each color scheme defines specific transfer functions for each channel, creating distinctive visual characteristics that can alter how the diffusion model interprets the noise structure.
+
+
+ C(t) = (R(t), G(t), B(t))
+
+
+
Key characteristics of the coloring process include:
+
+
Color Scheme Selection: Different schemes provide various perceptual encodings of the noise data.
+
Color Intensity: Controls how strongly the coloring affects the final noise blend, typically between 0.0 (no effect) and 1.0 (full effect).
+
Channel Blending: The colored information is blended into the noise channels before they influence the diffusion process.
+
+
+
+
+
+
+ The choice of color scheme significantly impacts how shader noise patterns guide the generative process. Different schemes create various perceptual encodings that can emphasize different aspects of the shader noise structure, potentially influencing the final image's mood, texture, and compositional elements.
+
+
+
Spectral Schemes - Emphasizing Transitions
+
+
+ Inferno
+
+
+ Magma
+
+
+ Plasma
+
+
+ Viridis
+
+
+
+
Technical Schemes - Enhancing Perception
+
+
+ Turbo
+
+
+ Jet
+
+
+ Parula
+
+
+ Rainbow
+
+
+ Hot
+
+
+
+
+
+
+
Applying Color Schemes
+
ShaderNoiseKSampler allows for the selection of various color schemes that are applied to the generated noise. The intensity of this coloration can also be controlled, determining how strongly the chosen color scheme influences the noise that guides the diffusion model.
+
+
+
Color Scheme Effects on Generation
+
+
Contrast Enhancement: Color schemes like Inferno and Jet can increase the perceived contrast in the noise pattern, potentially leading to more defined boundaries in the generated image.
+
Perceptual Organization: Schemes like Viridis and Turbo help to organize the noise data perceptually, which can influence how the model interprets spatial relationships.
+
Mood and Tone: The predominant hues of a color scheme can subtly influence the mood or tonal quality of the generated image, even when the colors themselves aren't directly visible.
+
Detail Emphasis: Some schemes are better at emphasizing small details within the noise pattern, which can lead to enhanced textural complexity in the final output.
+
+
+
+
+
+ Color Scheme Implementation
+
+
+
+# How color schemes are applied in the backend (simplified from curl_noise.py)
+def apply_color_scheme(normalized_value, color_scheme, device):
+ """
+ Apply a color scheme to normalized noise values (0-1 range).
+
+ Args:
+ normalized_value: Tensor of shape [batch, 1, height, width] with values in [0,1]
+ color_scheme: String identifier for the chosen color scheme
+ device: Computation device (cuda or cpu)
+
+ Returns:
+ Tuple of (r, g, b) channel tensors
+ """
+ # Helper function for lerping colors
+ def lerp(a, b, t):
+ return a + (b - a) * t
+
+ # Helper for color stops interpolation
+ def interpolate_colors(stops, t):
+ # stops: list of [value, color_tensor]
+ # Find which segment t falls into and interpolate
+ idx = torch.zeros_like(t, dtype=torch.long)
+ for i in range(len(stops) - 1):
+ idx = torch.where((t >= stops[i][0]) & (t < stops[i+1][0]),
+ torch.full_like(idx, i), idx)
+
+ # Handle edge case for t >= last stop
+ idx = torch.where(t >= stops[-1][0],
+ torch.full_like(idx, len(stops) - 2), idx)
+
+ # Initialize output tensor
+ final_color = torch.zeros_like(stops[0][1].expand(-1, -1, t.shape[2], t.shape[3]))
+
+ # Apply interpolation for each segment
+ for i in range(len(stops) - 1):
+ mask = (idx == i)
+ t0, c0 = stops[i]
+ t1, c1 = stops[i+1]
+
+ local_t = torch.clamp((t - t0) / (t1 - t0 + 1e-8), 0.0, 1.0)
+ segment_color = lerp(c0, c1, local_t)
+ final_color = torch.where(mask.expand_as(segment_color),
+ segment_color, final_color)
+
+ return final_color[:, 0:1], final_color[:, 1:2], final_color[:, 2:3]
+
+ # Different color schemes using the normalized value tensor
+ if color_scheme == "viridis":
+ stops = [
+ (0.0, torch.tensor([0.267, 0.005, 0.329], device=device).view(1, 3, 1, 1)), # #440154
+ (0.33, torch.tensor([0.188, 0.407, 0.553], device=device).view(1, 3, 1, 1)), # #30678D
+ (0.66, torch.tensor([0.208, 0.718, 0.471], device=device).view(1, 3, 1, 1)), # #35B778
+ (1.0, torch.tensor([0.992, 0.906, 0.143], device=device).view(1, 3, 1, 1)) # #FDE724
+ ]
+ r, g, b = interpolate_colors(stops, normalized_value)
+
+ elif color_scheme == "magma":
+ stops = [
+ (0.0, torch.tensor([0.001, 0.001, 0.016], device=device).view(1, 3, 1, 1)), # #000004
+ (0.25, torch.tensor([0.231, 0.059, 0.439], device=device).view(1, 3, 1, 1)), # #3B0F70
+ (0.5, torch.tensor([0.549, 0.161, 0.506], device=device).view(1, 3, 1, 1)), # #8C2981
+ (0.75, torch.tensor([0.871, 0.288, 0.408], device=device).view(1, 3, 1, 1)), # #DE4968
+ (0.85, torch.tensor([0.996, 0.624, 0.427], device=device).view(1, 3, 1, 1)), # #FE9F6D
+ (1.0, torch.tensor([0.988, 0.992, 0.749], device=device).view(1, 3, 1, 1)) # #FCFDBF
+ ]
+ r, g, b = interpolate_colors(stops, normalized_value)
+
+ elif color_scheme == "rainbow":
+ # Use HSV color space, with hue from the normalized value
+ h = normalized_value # Hue from normalized value [0,1]
+ s = torch.ones_like(h) * 0.8 # Fixed saturation
+ v = torch.ones_like(h) * 0.9 # Fixed value/brightness
+ r, g, b = hsv_to_rgb(h, s, v) # Convert to RGB
+
+ # ... other color schemes ...
+
+ else:
+ # Default grayscale if color scheme unknown
+ r = g = b = normalized_value
+
+ return r, g, b
+
+# Usage example in ShaderNoiseKSampler
+def apply_color_to_noise(noise_tensor, shader_params):
+ """Apply color scheme to noise tensor before blending with base noise"""
+ color_scheme = shader_params.get("color_scheme", "none")
+ color_intensity = shader_params.get("color_intensity", 0.8)
+
+ if color_scheme == "none" or color_intensity <= 0.0:
+ return noise_tensor # Skip coloring if disabled
+
+ # Normalize a channel (usually magnitude) for color mapping
+ channel_for_color = noise_tensor[:, 0:1] # Use first channel
+ normalized = torch.clamp((channel_for_color + 1.0) / 2.0, 0.0, 1.0) # Map [-1,1] to [0,1]
+
+ # Apply the color scheme
+ r, g, b = apply_color_scheme(normalized, color_scheme, noise_tensor.device)
+
+ # Create colored noise (first 3 channels get RGB, 4th unchanged or derived from magnitude)
+ colored_tensor = torch.cat([r, g, b, noise_tensor[:, 3:4]], dim=1)
+
+ # Blend with original noise based on intensity
+ if color_intensity < 1.0:
+ # Linear interpolation between original and colored noise
+ result = lerp(noise_tensor, colored_tensor, color_intensity)
+ return result
+ else:
+ return colored_tensor
+
+
+
+
+
+
+
+
+
๐ ๏ธ Usage ๐ง Working with this Tool
+
ShaderNoiseKSampler integrates advanced noise generation directly into the ComfyUI sampling process. It replaces the standard KSampler's noise generation with a sophisticated system that allows for multi-stage shader application, diverse noise types, transformations, and blending, offering fine-grained control over the creative output.
+
+
+
Core Workflow Integration
+
+
Connect Inputs:
+
+
model: The primary AI model for generation.
+
positive & negative: Conditioning prompts.
+
latent_image: The input latent to be processed.
+
+
+
Basic Sampling Parameters:
+
+
seed: For reproducibility use a fixed seed value for consistent results.
+
steps: Number of sampling iterations.
+
cfg: Classifier-Free Guidance scale.
+
sampler_name & scheduler: Standard KSampler choices.
+
denoise: Controls the extent of denoising.
+
+
+
Shader Noise Configuration: This is where ShaderNoiseKSampler shines:
+
+
sequential_stages: Number of shader stages applied sequentially over defined step ranges.
+
injection_stages: Number of shader stages applied at specific injection points during sampling.
+
shader_strength: Global strength of the shader noise influence. Set to 0.0 to use only base noise.
+
blend_mode: How shader noise combines with base noise (e.g., multiply, add, overlay).
+
noise_transform: Mathematical operations applied to the generated noise (e.g., absolute, sin, sqrt).
+
use_temporal_coherence: For generating frame-consistent noise, useful in animations.
+
+
+
(Optional) Advanced Control:
+
+
custom_sigmas: Provide a custom sigma schedule to override the model's default.
+
+
+
Run Workflow: Execute the ComfyUI graph. The ShaderNoiseKSampler will dynamically generate and blend noise according to your settings throughout the sampling process.
+
+
+
+
+
โ๏ธ Key Parameter Explanations
+
+
+
Sequential vs. Injection Stages
+
sequential_stages apply shader noise over segments of the diffusion process. The total steps are divided among these stages.
+ injection_stages apply shader noise at specific, discrete steps.
+ Sequential stages don't seem to add time to the generation but injection stages will add some time to the generation.
+
+
+
Shader Strength & Distributions
+
shader_strength is a global multiplier. Higher shader strength can lead to increased artifacting or generational oddities, but not always and not with all models or shader noise types. Small variations in shader strength is a good way to maintain the sum concepts of an image and not heavily vary its degree of difference.
+
+
+
Noise Transformation
+
The noise_transform (e.g., absolute, sin, sqrt) is a global setting applied to the generated shader noise within each stage before it's blended with the base noise. The chosen transformation is used consistently across all stages.
+
+
+
Temporal Coherence
+
When use_temporal_coherence is enabled, the node aims to generate consistent noise. For videos, this means noise evolves smoothly over a "time" dimension (often using 3D/4D noise with one dimension as time), with the base seed consistent across frames. For images, it ensures the noise pattern is consistently derived from the main seed, even with other parameter changes.
+
+
+
Custom Sigmas
+
custom_sigmas allows advanced users to define their own noise schedule (sigmas), giving more control over how noise is added and removed at each step. This can significantly alter the sampling dynamics.
+
+
+
Blend Mode
+
The blend_mode (e.g., multiply, add, overlay) is a global setting that determines how the shader noise combines with the base noise. This selected blend mode is used for all stages.
+
+
+
+
+
+
๐ง Creative Techniques & Advanced Usage
+
+
Shader Visualizer as a Guide: Using the shader visualizer in connection to the generation process is crucial for gaining insights into your parameter adjustments. The shader display was designed as a visual aid, not just for presentation. It helps you understand the shader noise\'s impact on the output image and serves as a navigational tool for latent space, mirroring backend shader parameters.
+
Detail through Noise Scale & Octaves: Higher noise scale and octaves can contribute to more detailed images. Essentially, a more "noisy" input increases the likelihood of observing finer-grained detail in the output.
+
Parameter Interdependence: Parameters are not always independent. If phase shift adjustments seem insufficient, try incorporating a bit of warp strength. Conversely, if your noise scale and octaves are already high, parameters like warp strength and phase shift might appear less impactful.
+
Color Intensity with Shape Masks: Color intensity is only effective when a shape mask is active. Once active, it can be used to introduce subtle to significant variations in your image, depending on the input values.
+
Shape Mask Strength: Similar to color intensity, shape mask strength is only usable when a shape mask is active. When enabled, it allows for small to large image variations based on your input values.
+
Warp & Phase with Active Masks/Colors: You might notice that when a color scheme or a shape mask (especially with higher strength/intensity) is active, the perceptual impact of warp_strength and phase_shift can seem diminished. This is likely because the structural influence of the mask or the channel modifications from coloring are significantly shaping the noise, potentially overshadowing the more subtle distortions or shifts from warp and phase. Experiment with moderating mask/color influence if you need to bring out the warp/phase effects more clearly.
+
Quick Ideation: For rapid idea generation without the need to constantly save parameters, consider using the (direct) version of this node. As is rapid parameter changes cannot be made to queue in succession, due to the way the node is currently implemented.
+
Saving Parameters: The "Save Shader Parameters" button (found on the ShaderNoiseKSampler node) allows you to save your current shader settings. These are saved to custom_nodes/ComfyUI-ShaderNoiseKsampler/data/shader_params.json. Note: this file must be named exactly shader_params.json in this specific directory and will be overwritten on each save. This is crucial for features that load these parameters. Use the (direct) version of this node to avoid saving parameters.
+
Shader Noise Palettes & Performance: Each Shader Noise Type (or palette) offers a distinct lens through which to explore the latent space, revealing different facets and possibilities within a seed. Most shader noise types process at roughly comparable speeds, typically adding only about 3-4 seconds to the generation time compared to a standard KSampler. While some may occasionally take a moment longer to initialize or load, the overall impact on generation time is generally minimal.
+
+
+
+
+
+
+
+
๐บ๏ธ Navigating Latent Space ๐งญ Shader Controls for Exploration
+
ShaderNoiseKSampler offers a way to explore latent space with more deliberate control compared to relying solely on random seeds. Shader parameters act as navigational tools, allowing users to chart the 'territory' within a given seed. This approach can lead to a more nuanced manipulation of the generative process, turning image generation into a more methodical exploration.
+
+
+
๐น๏ธ Parameter Deep Dive: Your Exploration Toolkit
+
Understanding how each core shader parameter influences the noise provides a map for your creative journey:
+
+
+
Noise Scale: The Zoom Control
+
Think of Noise Scale as the 'zoom lens' for your exploration within the latent space.
+
+
Low scale values: You're examining fine details and subtle variations within a small, localized region of the seed's potential. Ideal for nuanced adjustments.
+
High scale values: You're taking broader steps, observing more sweeping changes across a wider expanse. This can lead to more dramatic shifts in the output.
+
+
Effect on Generation: Controls how 'localized' or 'expansive' your exploration is. Smaller scales keep you closer to the seed's core characteristics, while larger scales venture further afield.
+
+
+
Octaves: The Detail Slider
+
Octaves determine the level of detail and complexity embedded within the noise pattern, and consequently, in the generated variations.
+
+
Lower octaves: Produce simpler, smoother noise, leading to more foundational or gentle variations of the theme.
+
Higher octaves: Introduce more intricate, layered patterns with multiple frequencies of detail, revealing complex elaborations and richer textures from the seed's inherent elements.
+
+
Effect on Generation: As you increase octaves, you're effectively adding finer and finer layers of detail, uncovering more complex permutations of the same underlying concepts.
+
+
+
Warp Strength: The Non-Linear Navigator
+
Warp Strength introduces non-linear distortions to the noise pattern, carving unconventional and often surprising paths through the latent space.
+
+
Low warp: Results in more predictable, somewhat linear modifications to the noise structure.
+
High warp: Creates significant distortions, 'folding,' and 'twisting' in the noise pattern. This allows you to unearth unusual or 'hidden' variations that might reside between the more straightforward pathways.
+
+
Effect on Generation: Enables the discovery of unique aesthetics by navigating the latent space in a less constrained, more fluid manner.
+
+
+
Phase Shift: The Perspective Shifter
+
Phase Shift functions like adjusting your viewpoint or the 'timing' of the noise pattern, subtly altering its manifestation in latent space.
+
+
Different phase values: Shift the entire noise pattern while generally maintaining its internal structural characteristics. It's akin to viewing the same intricate object from slightly different angles or under slightly different lighting conditions.
+
+
Effect on Generation: Reveals different 'facets' or 'expressions' of the same core elements within the seed's potential, offering a way to cycle through related variations smoothly.
+
+
+
Shape Mask Strength: The Selective Amplifier
+
Shape Mask Strength controls the intensity of geometric masks that modulate the noise pattern, allowing for spatial control.
+
+
Note: This parameter only has effect when a mask type is actively selected.
+
Small value changes: Even minor adjustments (0.01-0.05) can produce subtle yet noticeable variations in the final image, as they alter how the mask influences the noise structure.
+
+
Effect on Generation: Acts as a fine-tuning dial for how strongly the selected geometric pattern guides the noise formation, enabling controlled exploration of variations with spatial emphasis.
+
+
+
Color Intensity: The Chromatic Influence
+
Color Intensity determines how strongly the selected color scheme affects the noise pattern's structure and influence.
+
+
Note: Similar to Shape Mask Strength, this parameter only takes effect when a color scheme is enabled.
+
Small adjustments: Like shape mask strength, minor tweaks can subtly alter the generated outcome by changing how color information is integrated into the noise structure.
+
+
Effect on Generation: Provides nuanced control over how color information within the noise influences the diffusion process, allowing for exploration of variations with different color-driven structural emphasis.
+
+
+
+
+
+
+
+
๐ Potential of Guided Exploration
+
This methodical approach to navigating latent space using shader parameters can offer interesting possibilities:
+
+
Latent Space Cartography: You're not just generating images; you're effectively creating a map of the conceptual territory surrounding your chosen seed. Different parameter combinations become 'landmarks,' helping you develop an intuition for navigating towards specific aesthetic effects or thematic elements.
+
Persistent Identities in Variation: The remarkable tendency for similar elements (characters, objects, styles) to persist across various parameter adjustments suggests that certain semantic features are robustly encoded within the latent space neighborhood of a seed. This offers glimpses into how the model 'conceptualizes' and relates visual information.
+
Discovering "Hidden Gems": Traditional random seed exploration, by its nature, can easily miss nuanced or specific variations that exist in the 'gaps' between broadly different seeds. Deliberate navigation with shader parameters allows for the systematic discovery of these unique possibilities that random sampling might statistically overlook.
+
Creative Control Meets Serendipity: This methodology provides a powerful balance between intentional artistic direction and the joy of unexpected discovery. You're not rigidly dictating the output, nor are you entirely at the mercy of randomness; instead, you're skillfully steering your exploration through a rich space of possibilities.
+
Frequency Domain Exploration (Implicit): Given the mathematical underpinnings of shader noise (like Perlin, Fractal, etc.), you are, in essence, exploring how different frequency characteristics and patterns within the latent space map to tangible semantic features and visual styles in the generated images. This can reveal fundamental patterns in how the model encodes and interprets visual information.
+
+
+ "The true power lies in shifting from 'random sampling' to 'deliberate exploration'โ empowering artists to develop an intuition for navigating towards desired variations, rather than merely hoping to stumble upon them."
+
+
+
+
+
+
+
+
๐ Conclusion โฏ The Path of Creation
+
ShaderNoiseKSampler is a bridge between the mathematical sublime and practical artistry. By understanding and harnessing the sacred geometry of shader noise, artists can guide the generative process with precision and intentionality, transforming pure mathematical concepts into tools of creative expression.
+
It is important to note that this project represents a concept in its infancyโa minimal viable product of ongoing research. Much remains to be explored and understood in this fascinating intersection of mathematical noise patterns and generative art. The techniques and approaches presented here are foundational stepping stones in a journey that has only just begun.
+
This work constitutes original, independent investigation and implementationโa product of genuine curiosity and hands-on experimentation rather than an adaptation of existing research. The path forward relies on community support, both in collaborative exploration and financial backing, to fuel further innovation. By supporting grassroots research like this, you contribute directly to the advancement of creative technology driven by unfiltered exploration and authentic discovery.
+
May your journey with ShaderNoiseKSampler be one of discovery, inspiration, and creationโof digital tapestries woven from the very fabric of mathematical reality.
+
+ "In every shader noise pattern lies a universe of possibility
+ In every parameter, a doorway to creation"
+ - The Shader Matrix
+
+
+
+
+
+
+
+
+
+
+ `; // This line should end the template literal correctly
+ content.innerHTML = treatiseHTML;
+
+ // Attach listener to the close button *inside* the treatiseHTML
+ const closeButtonInTreatise = content.querySelector('.close-button');
+ if (closeButtonInTreatise) {
+ closeButtonInTreatise.onclick = (e) => {
+ e.stopPropagation();
+ closeModalCleanup();
+ };
+ }
+
+ modal.appendChild(content);
+ document.body.appendChild(modal);
+
+ // Call the renderer for noise visualizations
+ if (window.NoiseVisualizer && window.NoiseVisualizer.renderAllInModal) {
+ // Defer to ensure layout is complete and modal content is fully rendered
+ setTimeout(() => {
+ window.NoiseVisualizer.renderAllInModal(content); // 'content' is the div with class 'shader-matrix-treatise'
+ }, 0);
+ } else {
+ console.warn("NoiseVisualizer not found. Ensure noise_visualizer.js is loaded and available on the window object.");
+ }
+
+ // Initial scroll to top of modal content
+ content.scrollTop = 0;
+
+ // Activate the scroll-to-top button functionality
+ window.setupScrollTop(content); // Pass the 'content' element
+
+ });
+
+ // Add tooltip to the button
+ button.tooltip = "Show Shader Matrix & Documentation (Alt+M)";
+
+ // Position the button appropriately based on node type (from matrix_button - Copy.js)
+ if (nodeData.name === "ShaderNoiseKsampler") {
+ // For KSampler node, add to a specific section or position
+ if (!button.options) {
+ button.options = {};
+ }
+ button.options.section = "advanced";
+
+ // Custom button style for the KSampler node
+ button.label = "๐ Show Shader Matrix";
+ } else {
+ // Default styling for ShaderDisplay (label is already "Show Matrix")
+ // button.label = "Show Matrix"; // No change needed if initialized with "Show Matrix"
+ }
+
+ // Set button appearance (from matrix_button - Copy.js)
+ button.name = "๐ Show Shader Matrix";
+ button.serialize = false; // Don't include in serialization
+
+ // For ShaderDisplay, the button will use default positioning.
+ // For ShaderNoiseKsampler, without a section, it should append after other widgets.
+
+ };
+
+ // Call addMatrixButton conditionally (from matrix_button - Copy.js)
+ if (self.constructor.type_name === "ShaderNoiseKsampler") {
+ addMatrixButton();
+ } else {
+ // For other nodes, add after a small delay to ensure all widgets are ready
+ setTimeout(addMatrixButton, 50);
+ }
+
+ // --- Keybinding Logic for Matrix Button ---
+ const triggerMatrixButton = () => {
+ const matrixButtonWidget = self.widgets.find(w => w.name === "๐ Show Shader Matrix" && w.type === "button");
+ if (matrixButtonWidget && typeof matrixButtonWidget.callback === 'function') {
+ matrixButtonWidget.callback.call(matrixButtonWidget.value, app.canvas, self, null, null);
+ } else {
+ console.warn("Matrix button widget not found or callback is not a function for Alt+M.");
+ }
+ };
+
+ const handleMatrixKeyDown = (event) => {
+ if (event.altKey && event.key.toLowerCase() === 'm') {
+ if (app.canvas && (app.canvas.current_node === self || (app.canvas.selected_nodes && app.canvas.selected_nodes[self.id]))) {
+ if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA' || document.activeElement.isContentEditable)) {
+ return; // Don't interfere with text input
+ }
+ console.log("Alt+M detected for current/selected node to show matrix.");
+ event.preventDefault();
+ event.stopPropagation();
+ triggerMatrixButton();
+ }
+ }
+ };
+
+ document.addEventListener('keydown', handleMatrixKeyDown);
+ self.handleMatrixButtonKeyDown = handleMatrixKeyDown; // Store for removal
+
+ const originalOnRemoved = self.onRemoved;
+ self.onRemoved = function() {
+ if (self.handleMatrixButtonKeyDown) {
+ document.removeEventListener('keydown', self.handleMatrixButtonKeyDown);
+ delete self.handleMatrixButtonKeyDown;
+ console.log("Removed Alt+M keydown listener for matrix button on node:", self.id);
+ }
+ if (originalOnRemoved) {
+ originalOnRemoved.apply(self, arguments);
+ }
+ };
+ // --- End Keybinding Logic ---
+ }
+ }
+ });
+})();
\ No newline at end of file
diff --git a/web/noise_visualizer.js b/web/noise_visualizer.js
new file mode 100644
index 0000000..c23c31f
--- /dev/null
+++ b/web/noise_visualizer.js
@@ -0,0 +1,1608 @@
+// web/noise_visualizer.js
+if (!window.NoiseVisualizer) {
+ window.NoiseVisualizer = {
+ kofiCupImageBitmap: null, // To store the preloaded ImageBitmap object
+ kofiImageLoaded: false,
+ kofiImageLoadAttempted: false,
+
+ _preloadKofiImage: async function() { // Made async
+ if (this.kofiImageLoadAttempted) return;
+ this.kofiImageLoadAttempted = true;
+
+ const absolutePath = '/extensions/ComfyUI-ShaderNoiseKsampler/images/kofi_symbol.svg';
+
+ // Attempt 1: Direct Image load
+ try {
+ console.log("Attempting Ko-fi SVG load using new Image() with direct path (Primary Attempt)...");
+ const img = new Image();
+ await new Promise((resolve, reject) => {
+ img.onload = () => {
+ this.kofiCupImageBitmap = img; // Store the HTMLImageElement
+ this.kofiImageLoaded = true;
+ console.log("Ko-fi symbol SVG loaded via Image() successfully (Primary).");
+ resolve();
+ };
+ img.onerror = (e) => {
+ console.warn("Primary Ko-fi symbol SVG load via Image() failed. Proceeding to secondary attempt.", e);
+ reject(e); // Reject to proceed to the next try block
+ };
+ img.src = absolutePath;
+ });
+ return; // Successfully loaded, no need to proceed
+ } catch (error) {
+ // Error logged by the promise, clear flags before next attempt
+ this.kofiImageLoaded = false;
+ this.kofiCupImageBitmap = null;
+ }
+
+ // Attempt 2: Fetch -> Blob -> Intermediate Image -> ImageBitmap
+ let objectURL = null;
+ try {
+ console.log("Attempting Ko-fi SVG load via Blob -> Image -> ImageBitmap (Secondary Attempt)...");
+ const response = await fetch(absolutePath);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status} for ${absolutePath}`);
+ }
+ const svgText = await response.text();
+ const blob = new Blob([svgText], { type: 'image/svg+xml' });
+ objectURL = URL.createObjectURL(blob);
+
+ const intermediateImg = new Image();
+ await new Promise((resolve, reject) => {
+ intermediateImg.onload = resolve;
+ intermediateImg.onerror = (e) => reject(new Error("Intermediate Image() load failed for SVG blob (Secondary)."));
+ intermediateImg.src = objectURL;
+ });
+
+ this.kofiCupImageBitmap = await createImageBitmap(intermediateImg);
+ this.kofiImageLoaded = true;
+ console.log("Ko-fi symbol SVG processed to ImageBitmap successfully (Secondary).");
+
+ } catch (error) {
+ this.kofiImageLoaded = false;
+ this.kofiCupImageBitmap = null;
+ console.error("All Ko-fi SVG load attempts failed: ", error, ". Will use fallback drawing.");
+ } finally {
+ if (objectURL) {
+ URL.revokeObjectURL(objectURL); // Clean up the object URL
+ }
+ }
+ // If all attempts fail, kofiImageLoaded remains false, and manual drawing will be used.
+ },
+
+ renderAllInModal: async function(modalContentElement) { // Made async
+ if (!this.kofiImageLoadAttempted) {
+ await this._preloadKofiImage(); // Ensure preloading is complete
+ }
+
+ const noiseCanvases = modalContentElement.querySelectorAll('.noise-canvas');
+ noiseCanvases.forEach(canvasDiv => {
+ const canvasId = canvasDiv.id;
+ if (!canvasId || !canvasId.startsWith("noise-canvas-")) return;
+ const noiseType = canvasId.substring("noise-canvas-".length);
+
+ let canvas = canvasDiv.querySelector('canvas');
+ if (!canvas) {
+ canvas = document.createElement('canvas');
+ canvas.width = 130;
+ canvas.height = 130;
+ canvasDiv.innerHTML = '';
+ canvasDiv.appendChild(canvas);
+ }
+
+ const functionNameSuffix = noiseType.split('_').map(word => {
+ if (word.toLowerCase() === 'fbm') return 'FBM';
+ return word.charAt(0).toUpperCase() + word.slice(1);
+ }).join('');
+ const renderFunctionName = `render${functionNameSuffix.replace("3d", "3D")}`;
+
+ if (typeof this[renderFunctionName] === 'function') {
+ this[renderFunctionName](canvas);
+ } else {
+ console.warn("No renderer function found:", renderFunctionName, "for noise type:", noiseType);
+ this.renderPlaceholder(canvas, noiseType.replace(/_/g, ' '));
+ }
+ });
+
+ // Add rendering for mask canvases
+ const maskCanvases = modalContentElement.querySelectorAll('.mask-canvas');
+ maskCanvases.forEach(canvasDiv => {
+ const canvasId = canvasDiv.id;
+ if (!canvasId || !canvasId.startsWith("mask-canvas-")) return;
+ const maskType = canvasId.substring("mask-canvas-".length);
+
+ let canvas = canvasDiv.querySelector('canvas');
+ if (!canvas) {
+ canvas = document.createElement('canvas');
+ canvas.width = 100;
+ canvas.height = 70;
+ canvasDiv.innerHTML = '';
+ canvasDiv.appendChild(canvas);
+ }
+
+ const functionNameSuffix = maskType.split('_').map(word => {
+ return word.charAt(0).toUpperCase() + word.slice(1);
+ }).join('');
+ const renderFunctionName = `renderMask${functionNameSuffix}`;
+
+ if (typeof this[renderFunctionName] === 'function') {
+ this[renderFunctionName](canvas);
+ } else {
+ console.warn("No renderer function found:", renderFunctionName, "for mask type:", maskType);
+ this.renderMaskPlaceholder(canvas, maskType.replace(/_/g, ' '));
+ }
+ });
+
+ // Add rendering for color scheme swatches
+ const colorSwatches = modalContentElement.querySelectorAll('.color-swatch');
+ colorSwatches.forEach(swatchDiv => {
+ // Skip if it already has a gradient background
+ if (swatchDiv.style.background) return;
+
+ // Extract color scheme name from the div's text content
+ const schemeName = swatchDiv.textContent.trim().toLowerCase();
+ const renderFunctionName = `renderColor${schemeName.charAt(0).toUpperCase() + schemeName.slice(1)}`;
+
+ if (typeof this[renderFunctionName] === 'function') {
+ this[renderFunctionName](swatchDiv);
+ } else {
+ // For schemes without specific renderers, use the style already in the HTML
+ console.log("Using existing style for color scheme:", schemeName);
+ }
+ });
+
+ // Add rendering for the temporal animation demo
+ const animationDemoContainer = modalContentElement.querySelector('#animation-demo-placeholder');
+ if (animationDemoContainer) {
+ let canvas = animationDemoContainer.querySelector('canvas');
+ if (!canvas) {
+ canvas = document.createElement('canvas');
+ // Set canvas dimensions based on its container, respecting aspect ratio if possible
+ // For simplicity, let's use fixed dimensions or dimensions derived from container style
+ const containerStyle = getComputedStyle(animationDemoContainer);
+ canvas.width = parseInt(containerStyle.width) || 300; // Default if not specified
+ canvas.height = parseInt(containerStyle.height) || 200; // Default if not specified
+ animationDemoContainer.innerHTML = ''; // Clear placeholder text/content
+ animationDemoContainer.appendChild(canvas);
+ animationDemoContainer.style.display = 'block'; // Ensure container is block for canvas
+ }
+ if (typeof this.renderTemporalAnimation === 'function') {
+ this.renderTemporalAnimation(canvas);
+ } else {
+ console.warn("renderTemporalAnimation function not found in NoiseVisualizer.");
+ this.renderMaskPlaceholder(canvas, "Animation Demo"); // Fallback placeholder
+ }
+ }
+
+ // Add rendering for the intro noise demo
+ const introNoiseDemoContainer = modalContentElement.querySelector('#intro-noise-demo');
+ if (introNoiseDemoContainer) {
+ let canvas = introNoiseDemoContainer.querySelector('canvas');
+ if (!canvas) {
+ canvas = document.createElement('canvas');
+ const containerStyle = getComputedStyle(introNoiseDemoContainer);
+ canvas.width = parseInt(containerStyle.width) || 300;
+ canvas.height = parseInt(containerStyle.height) || 350; // Match defined height in CSS
+ introNoiseDemoContainer.innerHTML = ''; // Clear placeholder
+ introNoiseDemoContainer.appendChild(canvas);
+ introNoiseDemoContainer.style.display = 'block';
+ }
+ if (typeof this.renderIntroNoiseDemo === 'function') {
+ this.renderIntroNoiseDemo(canvas);
+ } else {
+ console.warn("renderIntroNoiseDemo function not found in NoiseVisualizer.");
+ this.renderMaskPlaceholder(canvas, "Intro Noise Demo");
+ }
+ }
+ },
+
+ _clearCanvas: function(canvas, backgroundColor = '#1a1a2e') {
+ const ctx = canvas.getContext('2d');
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ return ctx;
+ },
+
+ _drawKofiIcon: function(ctx) {
+ const iconSize = 18;
+ const padding = 3;
+ const x = ctx.canvas.width - iconSize - padding;
+ const y = ctx.canvas.height - iconSize - padding;
+
+ // Now checks for kofiCupImageBitmap
+ if (this.kofiImageLoaded && this.kofiCupImageBitmap) {
+ try {
+ // drawImage can take an ImageBitmap, HTMLImageElement, HTMLVideoElement, HTMLCanvasElement, etc.
+ ctx.drawImage(this.kofiCupImageBitmap, x, y, iconSize, iconSize);
+ } catch (e) {
+ console.error("Error drawing local Ko-fi SVG ImageBitmap, falling back to manual draw:", e);
+ this._drawManualKofiCup(ctx, x, y, iconSize);
+ }
+ } else {
+ this._drawManualKofiCup(ctx, x, y, iconSize);
+ }
+ },
+
+ _drawManualKofiCup: function(ctx, x, y, iconSize) {
+ ctx.save();
+ ctx.fillStyle = '#FFDD99';
+ ctx.strokeStyle = '#D2B48C';
+ ctx.lineWidth = 1;
+
+ ctx.beginPath();
+ ctx.moveTo(x, y + iconSize * 0.2);
+ ctx.lineTo(x, y + iconSize * 0.9);
+ ctx.quadraticCurveTo(x + iconSize * 0.5, y + iconSize * 1.1, x + iconSize, y + iconSize * 0.9);
+ ctx.lineTo(x + iconSize, y + iconSize * 0.2);
+ ctx.quadraticCurveTo(x + iconSize * 0.5, y, x, y + iconSize * 0.2);
+ ctx.fill();
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(x + iconSize * 0.9, y + iconSize * 0.5, iconSize * 0.25, -Math.PI/2, Math.PI/2);
+ ctx.stroke();
+
+ ctx.strokeStyle = '#FFFFFF';
+ ctx.lineWidth = 0.5;
+ ctx.beginPath();
+ ctx.moveTo(x + iconSize * 0.3, y + iconSize * 0.1);
+ ctx.quadraticCurveTo(x + iconSize * 0.2, y - iconSize * 0.2, x + iconSize * 0.4, y - iconSize * 0.3);
+ ctx.moveTo(x + iconSize * 0.6, y + iconSize * 0.05);
+ ctx.quadraticCurveTo(x + iconSize * 0.5, y - iconSize * 0.3, x + iconSize * 0.7, y - iconSize * 0.4);
+ ctx.stroke();
+
+ ctx.restore();
+ },
+
+ renderPlaceholder: function(canvas, noiseName) {
+ const ctx = this._clearCanvas(canvas, '#2c2c34');
+ const nameToShow = noiseName.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
+
+ ctx.fillStyle = '#e0e0e8';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.font = 'bold 12px "Segoe UI", Tahoma, Geneva, Verdana, sans-serif';
+
+ const words = nameToShow.split(' ');
+ if (words.length > 2) {
+ ctx.fillText(words.slice(0,2).join(' '), canvas.width / 2, canvas.height / 2 - 7);
+ ctx.fillText(words.slice(2).join(' '), canvas.width / 2, canvas.height / 2 + 7);
+ } else {
+ ctx.fillText(nameToShow, canvas.width / 2, canvas.height / 2);
+ }
+ ctx.strokeStyle = '#555';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ // --- Visualization Functions ---
+ renderTensorField: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.strokeStyle = 'rgba(138, 43, 226, 0.7)';
+ ctx.lineWidth = 1;
+ for (let i = 0; i < 10; i++) {
+ ctx.beginPath();
+ const y = (i + 0.5) * (canvas.height / 10);
+ ctx.moveTo(0, y);
+ for (let x = 0; x <= canvas.width; x += 5) {
+ const angle = Math.sin(x * 0.1 + y * 0.05) * Math.PI * 0.25;
+ ctx.lineTo(x + Math.cos(angle) * 5, y + Math.sin(angle) * 5);
+ ctx.moveTo(x,y);
+ }
+ ctx.stroke();
+ }
+ },
+
+ renderCellular: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const numCells = 8;
+ const points = [];
+ for (let i = 0; i < numCells; i++) {
+ points.push({x: Math.random() * canvas.width, y: Math.random() * canvas.height});
+ }
+ for (let x = 0; x < canvas.width; x += 4) {
+ for (let y = 0; y < canvas.height; y += 4) {
+ let minDist = Infinity;
+ points.forEach(p => { minDist = Math.min(minDist, Math.hypot(p.x - x, p.y - y)); });
+ const intensity = Math.min(255, minDist * 2);
+ ctx.fillStyle = `rgb(${intensity},${intensity},${intensity + 50})`;
+ ctx.fillRect(x, y, 4, 4);
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderDomainWarp: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.lineWidth = 1.5;
+ for (let i = 0; i < 20; i++) {
+ ctx.beginPath();
+ ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
+ ctx.strokeStyle = `rgba(${100 + Math.random()*155}, ${100 + Math.random()*155}, ${200 + Math.random()*55}, 0.6)`;
+ for (let j = 0; j < 5; j++) {
+ const x = Math.random() * canvas.width;
+ const y = Math.random() * canvas.height;
+ const cp1x = Math.random() * canvas.width;
+ const cp1y = Math.random() * canvas.height;
+ const cp2x = Math.random() * canvas.width;
+ const cp2y = Math.random() * canvas.height;
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
+ }
+ ctx.stroke();
+ }
+ },
+
+ renderFractal: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const scale = 0.05;
+ const octaves = 4;
+ const persistence = 0.5;
+ const lacunarity = 2.0;
+
+ for (let x = 0; x < canvas.width; x += 2) {
+ for (let y = 0; y < canvas.height; y += 2) {
+ let total = 0;
+ let frequency = 1;
+ let amplitude = 1;
+ let maxValue = 0;
+
+ for (let i = 0; i < octaves; i++) {
+ // Using a simple pseudo-random noise based on sin/cos
+ // A more robust noise function (like Simplex or Perlin) would be better for true FBM.
+ // This is a visual approximation for the small canvas.
+ const noiseVal = (Math.sin(x * scale * frequency + y * scale * frequency * 0.7) +
+ Math.cos(y * scale * frequency - x * scale * frequency * 0.3)) / 2;
+ total += noiseVal * amplitude;
+
+ maxValue += amplitude;
+ amplitude *= persistence;
+ frequency *= lacunarity;
+ }
+
+ const normalizedTotal = (total / maxValue + 1) / 2; // Normalize to 0-1 range
+ const colorVal = Math.floor(normalizedTotal * 200) + 55; // Map to 55-255 range
+
+ // A more neutral, less blue color scheme
+ ctx.fillStyle = `rgb(${colorVal}, ${colorVal * 0.95}, ${colorVal * 0.9})`;
+ ctx.fillRect(x, y, 2, 2);
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderPerlin: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let x = 0; x < canvas.width; x+=3) {
+ for (let y = 0; y < canvas.height; y+=3) {
+ const noiseVal = (Math.sin(x * 0.05 + Math.cos(y*0.08)) + Math.cos(y * 0.06)) / 2;
+ const intensity = (noiseVal + 1) / 2 * 200 + 55;
+ ctx.fillStyle = `rgba(${intensity * 0.8}, ${intensity * 0.9}, ${intensity}, 1)`;
+ ctx.fillRect(x, y, 3, 3);
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderWaves: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.strokeStyle = 'rgba(52, 152, 219, 0.7)';
+ ctx.lineWidth = 1.5;
+ for (let i = 0; i < 15; i++) {
+ ctx.beginPath();
+ const startY = i * (canvas.height / 15);
+ ctx.moveTo(0, startY);
+ for (let x = 0; x <= canvas.width; x += 5) {
+ const yOffset = Math.sin(x * 0.1 + i * 0.5) * 10;
+ ctx.lineTo(x, startY + yOffset);
+ }
+ ctx.stroke();
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderGaussian: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let i = 0; i < 10000; i++) {
+ const x = Math.random() * canvas.width;
+ const y = Math.random() * canvas.height;
+ const intensity = Math.floor(Math.random() * 100) + 100;
+ ctx.fillStyle = `rgba(${intensity * 0.8}, ${intensity * 0.9}, ${intensity}, ${Math.random() * 0.5 + 0.1})`;
+ ctx.fillRect(x - 1, y - 1, 2, 2);
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderHeterogeneousFBM: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let region = 0; region < 3; region++) {
+ const regionX = (canvas.width / 3) * region;
+ const regionW = canvas.width / 3;
+ const step = 2 + region * 2;
+ for (let x = 0; x < regionW; x += step) {
+ for (let y = 0; y < canvas.height; y += step) {
+ const noiseVal = Math.random();
+ const intensity = noiseVal * 255;
+ ctx.fillStyle = `rgb(${intensity}, ${intensity}, ${intensity})`;
+ ctx.fillRect(regionX + x, y, step, step);
+ }
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderInterference: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.lineWidth = 0.5;
+ const sources = [
+ {x: canvas.width * 0.2, y: canvas.height * 0.3, phase: 0},
+ {x: canvas.width * 0.8, y: canvas.height * 0.7, phase: Math.PI/2}
+ ];
+ for (let x = 0; x < canvas.width; x += 3) {
+ for (let y = 0; y < canvas.height; y += 3) {
+ let sum = 0;
+ sources.forEach(s => {
+ const dist = Math.hypot(s.x - x, s.y - y);
+ sum += Math.sin(dist * 0.1 + s.phase);
+ });
+ const intensity = (sum / sources.length + 1) / 2 * 255;
+ ctx.fillStyle = `rgb(${intensity * 0.7}, ${intensity}, ${intensity * 0.8})`;
+ ctx.fillRect(x, y, 3, 3);
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderSpectral: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let i = 0; i < canvas.height; i+=4) {
+ const freqComponent = Math.sin(i * 0.1) * 0.3 + Math.cos(i*0.05) * 0.3 + Math.random()*0.4;
+ const intensity = (freqComponent + 1)/2 * 255;
+ ctx.fillStyle = `rgb(${intensity}, ${intensity*0.8}, ${intensity*0.6})`;
+ ctx.fillRect(0, i, canvas.width, 4);
+ ctx.strokeStyle = 'rgba(0,0,0,0.2)';
+ ctx.strokeRect(0, i, canvas.width, 4);
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderProjection3D: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let i = 0; i < 50; i++) {
+ const z = Math.random();
+ const x = (Math.random() - 0.5) * canvas.width * (1 + z) + canvas.width / 2;
+ const y = (Math.random() - 0.5) * canvas.height * (1 + z) + canvas.height / 2;
+ const size = (1 - z) * 10 + 2;
+ const opacity = (1 - z) * 0.7 + 0.1;
+ ctx.fillStyle = `rgba(${150 + z*105}, ${150 + z*105}, ${200 + z*55}, ${opacity})`;
+ ctx.beginPath();
+ ctx.arc(x, y, size, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderCurlNoise: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.lineWidth = 1;
+ for (let i = 0; i < 50; i++) {
+ let x = Math.random() * canvas.width;
+ let y = Math.random() * canvas.height;
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ ctx.strokeStyle = `rgba(${100 + Math.random()*100}, ${150 + Math.random()*105}, ${200 + Math.random()*55}, 0.5)`;
+ for (let step = 0; step < 20; step++) {
+ const angle = Math.sin(x * 0.02 + y * 0.03) * Math.PI + Math.cos(y * 0.02 - x*0.01) * Math.PI;
+ x += Math.cos(angle) * 5;
+ y += Math.sin(angle) * 5;
+ if (x < 0 || x > canvas.width || y < 0 || y > canvas.height) break;
+ ctx.lineTo(x, y);
+ }
+ ctx.stroke();
+ }
+ },
+
+ // Mask Placeholder
+ renderMaskPlaceholder: function(canvas, maskName) {
+ const ctx = this._clearCanvas(canvas, '#2c2c34');
+ const nameToShow = maskName.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
+
+ ctx.fillStyle = '#e0e0e8';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.font = 'bold 10px "Segoe UI", Tahoma, Geneva, Verdana, sans-serif';
+ ctx.fillText(nameToShow, canvas.width / 2, canvas.height / 2);
+ ctx.strokeStyle = '#555';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ // Mask Visualization Functions
+ renderMaskRadial: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxRadius = Math.min(canvas.width, canvas.height) / 2;
+
+ // Create gradient for radial mask
+ const gradient = ctx.createRadialGradient(
+ centerX, centerY, 0,
+ centerX, centerY, maxRadius
+ );
+ gradient.addColorStop(0, 'rgba(255, 255, 255, 1.0)');
+ gradient.addColorStop(1, 'rgba(255, 255, 255, 0.0)');
+
+ // Draw the gradient as the mask
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskLinear: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+
+ // Create gradient for linear mask
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ gradient.addColorStop(0, 'rgba(255, 255, 255, 1.0)');
+ gradient.addColorStop(1, 'rgba(255, 255, 255, 0.0)');
+
+ // Draw the gradient as the mask
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskGrid: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const cellSize = 20;
+
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+
+ // Draw a grid pattern
+ for (let x = 0; x < canvas.width; x += cellSize) {
+ for (let y = 0; y < canvas.height; y += cellSize) {
+ if ((Math.floor(x / cellSize) + Math.floor(y / cellSize)) % 2 === 0) {
+ ctx.fillRect(x, y, cellSize, cellSize);
+ }
+ }
+ }
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskVignette: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxRadius = Math.sqrt(centerX * centerX + centerY * centerY);
+
+ // Create gradient for vignette mask (inverse of radial)
+ const gradient = ctx.createRadialGradient(
+ centerX, centerY, 0,
+ centerX, centerY, maxRadius
+ );
+ gradient.addColorStop(0, 'rgba(255, 255, 255, 0.0)');
+ gradient.addColorStop(0.6, 'rgba(255, 255, 255, 0.3)');
+ gradient.addColorStop(1, 'rgba(255, 255, 255, 1.0)');
+
+ // Draw the gradient as the mask
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskSpiral: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxRadius = Math.min(canvas.width, canvas.height) / 2;
+
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+ ctx.strokeStyle = 'rgba(255, 255, 255, 1.0)';
+ ctx.lineWidth = 5;
+
+ // Draw a spiral
+ ctx.beginPath();
+ for (let theta = 0; theta < 8 * Math.PI; theta += 0.1) {
+ const radius = (maxRadius / (8 * Math.PI)) * theta;
+ const x = centerX + radius * Math.cos(theta);
+ const y = centerY + radius * Math.sin(theta);
+
+ if (theta === 0) {
+ ctx.moveTo(x, y);
+ } else {
+ ctx.lineTo(x, y);
+ }
+ }
+ ctx.stroke();
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskHexgrid: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const hexSize = 15;
+ const hexHeight = hexSize * Math.sqrt(3);
+
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.lineWidth = 1;
+
+ // Draw a hexagonal grid
+ for (let row = 0; row < canvas.height / hexHeight + 1; row++) {
+ for (let col = 0; col < canvas.width / (hexSize * 3) + 1; col++) {
+ const offsetX = (row % 2) * hexSize * 1.5;
+ const x = col * hexSize * 3 + offsetX;
+ const y = row * hexHeight;
+
+ ctx.beginPath();
+ for (let i = 0; i < 6; i++) {
+ const angle = (i * Math.PI / 3);
+ const pX = x + hexSize * Math.cos(angle);
+ const pY = y + hexSize * Math.sin(angle);
+
+ if (i === 0) {
+ ctx.moveTo(pX, pY);
+ } else {
+ ctx.lineTo(pX, pY);
+ }
+ }
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ }
+ }
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskWavy: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+ ctx.strokeStyle = 'rgba(255, 255, 255, 1.0)';
+ ctx.lineWidth = 3;
+
+ const amplitude = 10;
+ const frequency = 0.05;
+
+ // Draw wavy lines
+ for (let y = 20; y < canvas.height; y += 20) {
+ ctx.beginPath();
+ for (let x = 0; x <= canvas.width; x += 2) {
+ const yOffset = Math.sin(x * frequency) * amplitude;
+ if (x === 0) {
+ ctx.moveTo(x, y + yOffset);
+ } else {
+ ctx.lineTo(x, y + yOffset);
+ }
+ }
+ ctx.stroke();
+ }
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskConcentricRings: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+
+ ctx.strokeStyle = 'rgba(255, 255, 255, 1.0)';
+ ctx.lineWidth = 2;
+
+ // Draw concentric rings
+ const ringCount = 6;
+ const maxRadius = Math.min(canvas.width, canvas.height) / 2;
+ const step = maxRadius / ringCount;
+
+ for (let i = 1; i <= ringCount; i++) {
+ const radius = i * step;
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
+ ctx.stroke();
+
+ // Add some fill for alternating rings
+ if (i % 2 === 0) {
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ // Add a subtle border around the canvas
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ // Color scheme rendering functions
+ renderColorInferno: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000004, #3b0f70, #8c2981, #de4968, #fe9f6d, #fcfdbf)';
+ },
+
+ renderColorMagma: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000004, #3b0f70, #8c2981, #de4968, #fe9f6d, #fcfdbf)';
+ },
+
+ renderColorPlasma: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0d0887, #7e03a8, #cc4678, #f89441, #f0f921)';
+ },
+
+ renderColorViridis: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #440154, #30678d, #35b778, #fde724)';
+ },
+
+ renderColorTurbo: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #30123b, #4669db, #26bf8c, #d4ff50, #fab74c, #ba0100)';
+ },
+
+ renderColorJet: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #00007f, #0000ff, #00ffff, #ffff00, #ff0000, #7f0000)';
+ },
+
+ renderColorParula: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #352a87, #0f5cdd, #00b5a6, #ffc337, #fcfea4)';
+ },
+
+ renderColorRainbow: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0000ff, #00ffff, #00ff00, #ffff00, #ff0000)';
+ },
+
+ renderColorHot: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #ff0000, #ffff00, #ffffff)';
+ },
+
+ renderColorBlueRed: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0000ff, #7777ff, #ffffff, #ff7777, #ff0000)';
+ },
+
+ renderColorCool: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #00ffff, #77aaff, #aa77ff, #ff00ff)';
+ },
+
+ renderColorHsv: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)';
+ },
+
+ renderColorAutumn: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #ff0000, #ff7700, #ffaa00, #ffdd00, #ffff00)';
+ },
+
+ renderColorWinter: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0000ff, #0077cc, #00aabb, #00ddaa)';
+ },
+
+ renderColorSpring: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #ff00ff, #ff33cc, #ff77aa, #ffaa77, #ffdd44, #ffff00)';
+ },
+
+ renderColorSummer: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #004433, #008855, #44aa66, #88cc77, #ccee88, #ffff66)';
+ },
+
+ renderColorCopper: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #331100, #662200, #993300, #cc6644, #ff9966)';
+ },
+
+ renderColorPink: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0a0a0a, #550055, #aa0066, #ff44aa, #ffaadd, #ffffff)';
+ },
+
+ renderColorBone: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #2a2a3a, #5a748a, #9ebacb, #dfdfef, #ffffff)';
+ },
+
+ renderColorOcean: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #000066, #0000bb, #0066cc, #00ccff, #99ffff)';
+ },
+
+ renderColorTerrain: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #333399, #009966, #66cc33, #cccc33, #ff9933, #ffffff)';
+ },
+
+ renderColorNeon: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #ff00ff, #aa00ff, #5500ff, #0000ff, #00aaff, #00ffff, #00ff00, #aaff00, #ffff00)';
+ },
+
+ renderColorFire: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #330000, #660000, #bb0000, #ff0000, #ff7700, #ffdd00, #ffffff)';
+ },
+
+ // New function for temporal animation
+ renderTemporalAnimation: function(canvas) {
+ const ctx = this._clearCanvas(canvas, '#0a0a0a'); // Dark background for animation
+ let time = 0;
+
+ function drawPattern(currentTime) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = '#0a0a0a';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ const numLines = 20;
+ const maxOffset = 20;
+
+ ctx.strokeStyle = 'rgba(52, 152, 219, 0.6)';
+ ctx.lineWidth = 1.5;
+
+ for (let i = 0; i < numLines; i++) {
+ ctx.beginPath();
+ const startY = (i / numLines) * canvas.height;
+ ctx.moveTo(0, startY);
+ for (let x = 0; x <= canvas.width; x += 5) {
+ // Create a wave that evolves with time and differs per line
+ const yOffset = Math.sin(x * 0.02 + i * 0.3 + currentTime * 0.05) * maxOffset * Math.sin(currentTime * 0.02 + i * 0.1);
+ const actualY = startY + yOffset;
+ ctx.lineTo(x, actualY);
+ }
+ ctx.stroke();
+ }
+ }
+
+ function animate() {
+ drawPattern(time);
+ time += 0.1; // Increment time for animation
+ // Only request next frame if the canvas is still in the document (modal is open)
+ if (canvas.isConnected) {
+ requestAnimationFrame(animate);
+ }
+ }
+
+ // Check if an animation is already running on this canvas
+ if (!canvas.dataset.animationRunning) {
+ canvas.dataset.animationRunning = 'true';
+ animate();
+ }
+ },
+
+ // New function for the intro noise demo
+ renderIntroNoiseDemo: function(canvas) {
+ const ctx = this._clearCanvas(canvas, '#0f0f12'); // Darker background for intro
+ let time = 0;
+ const particles = [];
+ const numParticles = 120; // Slightly reduced for clarity with trails
+ const particleColorBase = [138, 43, 226];
+ const particleColorHighlight = [224, 224, 232];
+ const loopCycleDuration = 420; // Extended to 7 seconds
+ let lastLoopInstance = -1;
+ let currentBackgroundPattern = { type: null, variantSeed: 0, alpha: 0, startTime: 0 };
+
+ // Helper function to get spawn points based on pattern
+ function getSpawnPointForPattern(patternType, variantSeed, canvas, patternTimeVal) {
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxPatternSize = Math.min(centerX, centerY) * 0.95;
+ let spawnX = Math.random() * canvas.width;
+ let spawnY = Math.random() * canvas.height;
+
+ // timeVal for spawning should reflect the pattern at its initial state (or very early evolution)
+ const timeVal = patternTimeVal * 0.00015; // Match background pattern evolution rate
+
+ if (patternType === 'tensor_field') {
+ const gridSize = 30;
+ const gx = Math.floor(Math.random() * (canvas.width / gridSize)) * gridSize + gridSize/2;
+ const gy = Math.floor(Math.random() * (canvas.height / gridSize)) * gridSize + gridSize/2;
+ const tfScale = 0.025 + (variantSeed % 5) * 0.003;
+ const angleOffset = variantSeed * 0.3;
+ const fieldAngle = Math.sin(gx * tfScale + timeVal * 5 + angleOffset) * Math.PI +
+ Math.cos(gy * tfScale - timeVal * 3) * Math.PI;
+ const fieldMagnitude = (Math.sin(gx * tfScale * 0.7 + gy * tfScale * 0.5 - timeVal * 2) + 1) / 2;
+ const lineLength = 10 * (0.5 + fieldMagnitude * 0.7);
+ // Spawn along the vector line
+ const r = Math.random() * lineLength - lineLength/2;
+ spawnX = gx + Math.cos(fieldAngle) * r;
+ spawnY = gy + Math.sin(fieldAngle) * r;
+ } else if (patternType === 'cellular') {
+ const numPoints = 3 + Math.floor(variantSeed % 4);
+ const pointIndex = Math.floor(Math.random() * numPoints);
+ const angle = (pointIndex / numPoints) * Math.PI * 2 + timeVal * (0.1 + (variantSeed % 3)*0.03) + variantSeed * 0.7;
+ const radius = maxPatternSize * (0.4 + ((pointIndex + variantSeed) % 4) * 0.12) * (0.85 + Math.sin(timeVal*1.5 + pointIndex) * 0.15);
+ const pX = centerX + Math.cos(angle) * radius;
+ const pY = centerY + Math.sin(angle) * radius;
+ // Spawn near a cell center point
+ spawnX = pX + (Math.random() - 0.5) * 30;
+ spawnY = pY + (Math.random() - 0.5) * 30;
+ } else if (patternType === 'domain_warp') {
+ const gridSize = 25;
+ const c = Math.floor(Math.random() * (canvas.width / gridSize));
+ const r = Math.floor(Math.random() * (canvas.height / gridSize));
+ const origX = c * gridSize;
+ const origY = r * gridSize;
+ const warpScale = 0.015 + (variantSeed % 5) * 0.002;
+ const warpIntensity = canvas.width * (0.05 + (variantSeed % 3) * 0.02);
+ const offsetX = Math.sin(origY * warpScale * 1.2 + timeVal * 8 + variantSeed) * warpIntensity;
+ const offsetY = Math.cos(origX * warpScale * 0.8 - timeVal * 6 + variantSeed) * warpIntensity;
+ spawnX = origX + offsetX;
+ spawnY = origY + offsetY;
+ } else if (patternType === 'perlin') {
+ // Try a few random spots and pick one with mid-range noise intensity
+ let bestX = spawnX, bestY = spawnY, minDiff = Infinity;
+ for(let i=0; i<10; i++) {
+ let sx = Math.random() * canvas.width;
+ let sy = Math.random() * canvas.height;
+ let noiseVal = 0;
+ let freq = 0.015 + (variantSeed % 7) * 0.003;
+ let amp = 0.6;
+ for (let oct = 0; oct < 3; oct++) {
+ noiseVal += Math.sin(sx * freq + timeVal*10 + variantSeed*0.2) * Math.cos(sy * freq - timeVal*12 - variantSeed*0.1) * amp;
+ freq *= 1.8 + (variantSeed % 3) * 0.1;
+ amp *= 0.45 + (variantSeed % 4) * 0.02;
+ }
+ const currentDiff = Math.abs(noiseVal); // Target noiseVal around 0 (mid-range for bipolar noise)
+ if (currentDiff < minDiff) {
+ minDiff = currentDiff;
+ bestX = sx; bestY = sy;
+ }
+ }
+ spawnX = bestX; spawnY = bestY;
+ } else if (patternType === 'curl_noise') {
+ // Spawn along a few conceptual streamlines
+ const numStreamlines = 5;
+ const streamlineIndex = Math.floor(Math.random() * numStreamlines);
+ let pX = canvas.width * (streamlineIndex / numStreamlines + Math.random() * 0.1 - 0.05);
+ let pY = canvas.height * Math.random();
+ const curlScale = 0.018 + (variantSeed % 5) * 0.002;
+ const Gx_val = Math.sin(pY*curlScale + timeVal*7 + variantSeed + streamlineIndex*0.1);
+ const Gy_val = Math.cos(pX*curlScale - timeVal*9 + variantSeed + streamlineIndex*0.1);
+ const flowX = Gy_val;
+ const flowY = -Gx_val;
+ const norm = Math.hypot(flowX, flowY);
+ if (norm > 0.01) {
+ const step = (Math.random() - 0.5) * 50; // Spawn somewhere along the local flow vector
+ spawnX = pX + (flowX / norm) * step;
+ spawnY = pY + (flowY / norm) * step;
+ } else {
+ spawnX = pX; spawnY = pY;
+ }
+ } else if (patternType === 'domain_warp') {
+ const gridSize = 25;
+ const c = Math.floor(Math.random() * (canvas.width / gridSize));
+ const r = Math.floor(Math.random() * (canvas.height / gridSize));
+ const origX = c * gridSize;
+ const origY = r * gridSize;
+ const warpScale = 0.015 + (variantSeed % 5) * 0.002;
+ const warpIntensity = canvas.width * (0.05 + (variantSeed % 3) * 0.02);
+ const offsetX = Math.sin(origY * warpScale * 1.2 + timeVal * 8 + variantSeed) * warpIntensity;
+ const offsetY = Math.cos(origX * warpScale * 0.8 - timeVal * 6 + variantSeed) * warpIntensity;
+ spawnX = origX + offsetX;
+ spawnY = origY + offsetY;
+ } else if (patternType === 'perlin') {
+ // Try a few random spots and pick one with mid-range noise intensity
+ let bestX = spawnX, bestY = spawnY, minDiff = Infinity;
+ for(let i=0; i<10; i++) {
+ let sx = Math.random() * canvas.width;
+ let sy = Math.random() * canvas.height;
+ let noiseVal = 0;
+ let freq = 0.015 + (variantSeed % 7) * 0.003;
+ let amp = 0.6;
+ for (let oct = 0; oct < 3; oct++) {
+ noiseVal += Math.sin(sx * freq + timeVal*10 + variantSeed*0.2) * Math.cos(sy * freq - timeVal*12 - variantSeed*0.1) * amp;
+ freq *= 1.8 + (variantSeed % 3) * 0.1;
+ amp *= 0.45 + (variantSeed % 4) * 0.02;
+ }
+ const currentDiff = Math.abs(noiseVal); // Target noiseVal around 0 (mid-range for bipolar noise)
+ if (currentDiff < minDiff) {
+ minDiff = currentDiff;
+ bestX = sx; bestY = sy;
+ }
+ }
+ spawnX = bestX; spawnY = bestY;
+ } else if (patternType === 'curl_noise') {
+ // Spawn along a few conceptual streamlines
+ const numStreamlines = 5;
+ const streamlineIndex = Math.floor(Math.random() * numStreamlines);
+ let pX = canvas.width * (streamlineIndex / numStreamlines + Math.random() * 0.1 - 0.05);
+ let pY = canvas.height * Math.random();
+ const curlScale = 0.018 + (variantSeed % 5) * 0.002;
+ const Gx_val = Math.sin(pY*curlScale + timeVal*7 + variantSeed + streamlineIndex*0.1);
+ const Gy_val = Math.cos(pX*curlScale - timeVal*9 + variantSeed + streamlineIndex*0.1);
+ const flowX = Gy_val;
+ const flowY = -Gx_val;
+ const norm = Math.hypot(flowX, flowY);
+ if (norm > 0.01) {
+ const step = (Math.random() - 0.5) * 50; // Spawn somewhere along the local flow vector
+ spawnX = pX + (flowX / norm) * step;
+ spawnY = pY + (flowY / norm) * step;
+ } else {
+ spawnX = pX; spawnY = pY;
+ }
+ } else if (patternType === 'waves_interference') {
+ const numWaveSets = 2 + Math.floor(variantSeed % 2);
+ const ws = Math.floor(Math.random()*numWaveSets); // Pick a wave set
+ const lineIdx = Math.floor(Math.random()*15); // Pick a line within the set
+
+ const amplitude = maxPatternSize * (0.15 + (variantSeed % 3) * 0.05);
+ const frequency = 0.025 + ((variantSeed+ws*2) % 7) * 0.004;
+ const angle = (variantSeed * 0.2 + ws * Math.PI / (numWaveSets * 1.5)) + timeVal * (3 + ws*0.5);
+ const cosA = Math.cos(angle);
+ const sinA = Math.sin(angle);
+
+ const lineOffset = (lineIdx - 15/2) * maxPatternSize * 0.1;
+ const j = (Math.random() - 0.5) * maxPatternSize * 2.0; // Random point along the line
+ const waveDisplacement = Math.sin(j * frequency + timeVal * (10 + ws*3) + ws*Math.PI) * amplitude;
+ const originalX = j;
+ const originalY = lineOffset + waveDisplacement;
+
+ spawnX = centerX + (originalX * cosA - originalY * sinA);
+ spawnY = centerY + (originalX * sinA + originalY * cosA);
+ }
+
+ return {
+ x: Math.max(0, Math.min(canvas.width, spawnX)),
+ y: Math.max(0, Math.min(canvas.height, spawnY))
+ };
+ }
+
+ // Noise function modified to include influence from background patterns
+ function noiseField(x, y, t, patternType, patternVariantSeed, patternTime) {
+ const baseScale = 0.009;
+ const timeScale = 0.0023;
+ const loopTime = t % loopCycleDuration;
+ const currentLoopInstance = Math.floor(t / loopCycleDuration);
+
+ const globalRotationAngle = currentLoopInstance * 0.15;
+ const cosA = Math.cos(globalRotationAngle);
+ const sinA = Math.sin(globalRotationAngle);
+
+ let rotatedX = x * cosA - y * sinA;
+ let rotatedY = x * sinA + y * cosA;
+
+ // --- Base Chaotic Noise Calculation (similar to before) ---
+ let baseChaosVal = 0;
+ const scale1 = baseScale * 1.8;
+ baseChaosVal += Math.sin(rotatedX * scale1 + loopTime * timeScale * 1.1) *
+ Math.cos(rotatedY * scale1 * 0.8 - loopTime * timeScale * 0.9);
+ const scale2 = baseScale * 0.9;
+ baseChaosVal += Math.sin(rotatedY * scale2 * 1.1 - loopTime * timeScale * 1.3) *
+ Math.cos(rotatedX * scale2 * 0.9 + loopTime * timeScale * 1.0) * 0.8;
+ const scale3 = baseScale * 1.2;
+ const innerAngleOffset = Math.sin(loopTime * 0.0008 + currentLoopInstance * 0.05) * 0.5;
+ const N_x_chaos = rotatedX * Math.cos(innerAngleOffset) - rotatedY * Math.sin(innerAngleOffset);
+ const N_y_chaos = rotatedX * Math.sin(innerAngleOffset) + rotatedY * Math.cos(innerAngleOffset);
+ baseChaosVal += Math.sin(N_x_chaos * scale3 * 0.6 + loopTime * timeScale * 0.7) *
+ Math.cos(N_y_chaos * scale3 * 0.7 - loopTime * timeScale * 0.5) * 0.9;
+ let baseAngle = (baseChaosVal / 2.7) * Math.PI * 4.0; // Base movement angle
+
+ // --- Background Pattern Influence Calculation ---
+ let influenceAngle = baseAngle;
+ let influenceStrength = 0.0; // 0.0 to 1.0 (max influence)
+ const patternEvoTime = patternTime * 0.00015;
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+
+ if (patternType === 'tensor_field') {
+ const tfScale = 0.025 + (patternVariantSeed % 5) * 0.003;
+ const angleOffset = patternVariantSeed * 0.3;
+ // Match the angle calculation from drawBackgroundShaderPattern
+ influenceAngle = Math.sin(x * tfScale + patternEvoTime * 5 + angleOffset) * Math.PI +
+ Math.cos(y * tfScale - patternEvoTime * 3) * Math.PI;
+ influenceStrength = 0.75;
+ } else if (patternType === 'cellular') {
+ const numPoints = 3 + Math.floor(patternVariantSeed % 4);
+ const points = [];
+ for (let i = 0; i < numPoints; i++) {
+ const angle = (i / numPoints) * Math.PI * 2 + patternEvoTime * (0.1 + (patternVariantSeed % 3)*0.03) + patternVariantSeed * 0.7;
+ const radiusFactor = (0.4 + ((i + patternVariantSeed) % 4) * 0.12) * (0.85 + Math.sin(patternEvoTime*1.5 + i) * 0.15);
+ points.push({
+ x: centerX + Math.cos(angle) * (Math.min(centerX,centerY)*0.95) * radiusFactor,
+ y: centerY + Math.sin(angle) * (Math.min(centerX,centerY)*0.95) * radiusFactor
+ });
+ }
+
+ let p1_idx = -1, p2_idx = -1;
+ let minDist1 = Infinity, minDist2 = Infinity;
+
+ for(let k=0; kpB, flow one way, else other.
+ // Vector from pA to particle (particleRelX, particleRelY)
+ // Vector pA to pB (edgeDx, edgeDy)
+ // 2D cross product: particleRelX * edgeDy - particleRelY * edgeDx
+ if (particleRelX * edgeDy - particleRelY * edgeDx > 0) {
+ influenceAngle += Math.PI; // Reverse direction
+ }
+ }
+ influenceStrength = 0.7;
+ } else {
+ // If not near an edge, maybe weakly attract to the closest point or a mix?
+ // For now, let's stick to edge following for clear "lines"
+ influenceStrength = 0.1; // Weak influence if not near edge
+ }
+ }
+ } else if (patternType === 'domain_warp') {
+ const warpScale = 0.015 + (patternVariantSeed % 5) * 0.002;
+ const warpIntensity = 25 + (patternVariantSeed % 3) * 5;
+ const displacementX = Math.sin(y * warpScale * 1.2 + patternEvoTime * 8 + patternVariantSeed) * warpIntensity;
+ const displacementY = Math.cos(x * warpScale * 0.8 - patternEvoTime * 6 + patternVariantSeed) * warpIntensity;
+ if (Math.hypot(displacementX, displacementY) > 0.1) {
+ influenceAngle = Math.atan2(displacementY, displacementX);
+ influenceStrength = 0.7; // Increased strength
+ }
+ } else if (patternType === 'perlin') {
+ let noiseValCenter = 0, noiseValX = 0, noiseValY = 0;
+ let freq = 0.015 + (patternVariantSeed % 7) * 0.003;
+ let amp = 0.6;
+ for (let oct = 0; oct < 3; oct++) {
+ noiseValCenter += Math.sin(x * freq + patternEvoTime*10 + patternVariantSeed*0.2) * Math.cos(y * freq - patternEvoTime*12 - patternVariantSeed*0.1) * amp;
+ noiseValX += Math.sin((x+1) * freq + patternEvoTime*10+ patternVariantSeed*0.2) * Math.cos(y * freq - patternEvoTime*12 - patternVariantSeed*0.1) * amp;
+ noiseValY += Math.sin(x * freq + patternEvoTime*10+ patternVariantSeed*0.2) * Math.cos((y+1) * freq - patternEvoTime*12 - patternVariantSeed*0.1) * amp;
+ freq *= 1.8 + (patternVariantSeed % 3) * 0.1;
+ amp *= 0.45 + (patternVariantSeed % 4) * 0.02;
+ }
+ const gradX = noiseValX - noiseValCenter;
+ const gradY = noiseValY - noiseValCenter;
+ if (Math.hypot(gradX, gradY) > 0.005) {
+ influenceAngle = Math.atan2(gradY, gradX) + (Math.PI / 2); // Perpendicular to gradient
+ influenceStrength = 0.7; // Increased strength
+ }
+ } else if (patternType === 'waves_interference') {
+ let waveSum = 0;
+ const numWaveSources = 2 + (patternVariantSeed % 2);
+ for(let i=0; i 0.01) {
+ influenceAngle = Math.atan2(gradY, gradX) + (Math.PI / 2); // Perpendicular to gradient
+ influenceStrength = 0.65;
+ }
+ } else if (patternType === 'curl_noise') {
+ const curlScale = 0.018 + (patternVariantSeed % 5) * 0.002;
+ const Gx_val = Math.sin(y*curlScale + patternEvoTime*7 + patternVariantSeed);
+ const Gy_val = Math.cos(x*curlScale - patternEvoTime*9 + patternVariantSeed);
+ const flowX = Gy_val;
+ const flowY = -Gx_val;
+ if (Math.hypot(flowX, flowY) > 0.01) {
+ influenceAngle = Math.atan2(flowY, flowX);
+ influenceStrength = 0.85; // Increased strength
+ }
+ }
+
+ // Blend base chaos with pattern influence
+ // Normalize angles for smoother blending if they wrap around PI
+ let finalAngle;
+ let diff = influenceAngle - baseAngle;
+ while (diff > Math.PI) diff -= 2 * Math.PI;
+ while (diff < -Math.PI) diff += 2 * Math.PI;
+ finalAngle = baseAngle + diff * influenceStrength;
+
+ return finalAngle;
+ }
+
+ for (let i = 0; i < numParticles; i++) {
+ particles.push({
+ x: Math.random() * canvas.width,
+ y: Math.random() * canvas.height,
+ vx: 0,
+ vy: 0,
+ life: 0, // Start dead, will be revived in primordial state
+ initialLife: Math.random() * 100 + 90,
+ radius: Math.random() * 1.7 + 0.6,
+ trail: [],
+ isPrimordial: true, // New flag
+ primordialTime: 0 // Counter for primordial phase
+ });
+ }
+ const primordialDuration = 60; // Approx 1 second for primordial phase
+
+ function drawBackgroundShaderPattern(ctx, type, variantSeed, targetAlpha, patternTime) {
+ ctx.save();
+ ctx.globalAlpha = targetAlpha * 0.07; // Slightly increased base visibility for new patterns
+ ctx.strokeStyle = 'rgba(180, 190, 200, 0.75)';
+ ctx.lineWidth = 0.7;
+
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxPatternSize = Math.min(centerX, centerY) * 0.95;
+ const timeVal = patternTime * 0.00015; // Consistent slow evolution
+
+ if (type === 'cellular') {
+ const numPoints = 3 + Math.floor(variantSeed % 4); // 3 to 6 points
+ const points = [];
+ for (let i = 0; i < numPoints; i++) {
+ const angle = (i / numPoints) * Math.PI * 2 + timeVal * (0.1 + (variantSeed % 3)*0.03) + variantSeed * 0.7;
+ const radius = maxPatternSize * (0.4 + ((i + variantSeed) % 4) * 0.12) * (0.85 + Math.sin(timeVal*1.5 + i) * 0.15);
+ points.push({
+ x: centerX + Math.cos(angle) * radius,
+ y: centerY + Math.sin(angle) * radius
+ });
+ }
+ // Draw Voronoi edges (simplified)
+ ctx.beginPath();
+ const step = 15;
+ for (let cy = 0; cy < canvas.height; cy += step) {
+ for (let cx = 0; cx < canvas.width; cx += step) {
+ let minDist1 = Infinity, minDist2 = Infinity;
+ let p1_idx = -1;
+ for(let k=0; k canvas.width || pY < 0 || pY > canvas.height) break;
+ ctx.lineTo(pX,pY);
+ }
+ ctx.stroke();
+ }
+ }
+
+ ctx.restore();
+ }
+
+ function drawIntroPattern() {
+ const currentLoopInstance = Math.floor(time / loopCycleDuration);
+ const isNewLoopStart = (time % loopCycleDuration) < (60 / 2.5);
+
+ if (isNewLoopStart && currentLoopInstance !== lastLoopInstance) {
+ ctx.fillStyle = 'rgba(15, 15, 18, 0.45)';
+ lastLoopInstance = currentLoopInstance;
+
+ const shaderPatternTypes = ['tensor_field', 'cellular', 'domain_warp', 'perlin', 'curl_noise', 'waves_interference'];
+ currentBackgroundPattern.type = shaderPatternTypes[currentLoopInstance % shaderPatternTypes.length];
+ currentBackgroundPattern.variantSeed = currentLoopInstance; // Use loop instance for variation
+ currentBackgroundPattern.alpha = 1.0;
+ currentBackgroundPattern.startTime = time;
+
+ const patternSpawnTimeVal = 0; // Use 0 for the conceptual "start time" of the pattern for consistent spawning
+
+ particles.forEach(p => {
+ const spawnPoint = getSpawnPointForPattern(currentBackgroundPattern.type, currentBackgroundPattern.variantSeed, canvas, patternSpawnTimeVal);
+ p.x = spawnPoint.x;
+ p.y = spawnPoint.y;
+ // p.x = Math.random() * canvas.width; // Old random spawn
+ // p.y = Math.random() * canvas.height; // Old random spawn
+ p.vx = (Math.random() - 0.5) * 0.1;
+ p.vy = (Math.random() - 0.5) * 0.1;
+ p.life = p.initialLife;
+ p.trail = [];
+ p.isPrimordial = true;
+ p.primordialTime = 0;
+ });
+
+ } else {
+ ctx.fillStyle = 'rgba(15, 15, 18, 0.015)';
+ }
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ if (currentBackgroundPattern.type && currentBackgroundPattern.alpha > 0) {
+ drawBackgroundShaderPattern(ctx, currentBackgroundPattern.type, currentBackgroundPattern.variantSeed, currentBackgroundPattern.alpha, time - currentBackgroundPattern.startTime);
+ currentBackgroundPattern.alpha = Math.max(0, 1.0 - ((time - currentBackgroundPattern.startTime) / loopCycleDuration) * 0.95 ); // Adjusted fade for longer loop
+ }
+
+ particles.forEach(p => {
+ if (p.isPrimordial) {
+ p.primordialTime++;
+ if (p.primordialTime >= primordialDuration) {
+ p.isPrimordial = false;
+ }
+ }
+
+ const finalAngle = noiseField(p.x, p.y, time, currentBackgroundPattern.type, currentBackgroundPattern.variantSeed, time - currentBackgroundPattern.startTime);
+ // const angle = noiseVal * Math.PI * 4.0; // Old way
+
+ p.vx += Math.cos(finalAngle) * 0.10;
+ p.vy += Math.sin(finalAngle) * 0.10;
+
+ p.vx *= 0.94;
+ p.vy *= 0.94;
+
+ p.x += p.vx;
+ p.y += p.vy;
+
+ p.trail.push({x: p.x, y: p.y});
+ if (p.trail.length > 25) {
+ p.trail.shift();
+ }
+
+ let trailBaseAlpha = 0.8;
+ let particleHeadBaseAlpha = 0.65;
+ let currentParticleRadius = p.radius;
+ let r = particleColorBase[0], g = particleColorBase[1], b = particleColorBase[2];
+
+ if (p.isPrimordial) {
+ const primordialRatio = p.primordialTime / primordialDuration;
+ trailBaseAlpha *= primordialRatio * 0.5; // Trails fainter during primordial
+ particleHeadBaseAlpha = primordialRatio * 0.5;
+ currentParticleRadius = p.radius * (0.3 + primordialRatio * 0.7); // Grow from small
+ // Desaturated primordial color (e.g., grayish-purple)
+ const primordialColorLerp = primordialRatio;
+ r = (particleColorBase[0] * primordialColorLerp) + (70 * (1 - primordialColorLerp));
+ g = (particleColorBase[1] * primordialColorLerp) + (70 * (1 - primordialColorLerp));
+ b = (particleColorBase[2] * primordialColorLerp) + (90 * (1 - primordialColorLerp));
+ }
+
+ if (p.trail.length > 1) {
+ ctx.beginPath();
+ ctx.moveTo(p.trail[0].x, p.trail[0].y);
+ for (let i = 1; i < p.trail.length; i++) {
+ const trailSegmentAlpha = (i / p.trail.length) * trailBaseAlpha;
+ const trailColor = `rgba(200, 200, 200, ${trailSegmentAlpha})`;
+ ctx.strokeStyle = trailColor;
+ ctx.lineWidth = currentParticleRadius * (i / p.trail.length) * 1.1;
+ ctx.lineTo(p.trail[i].x, p.trail[i].y);
+ }
+ ctx.stroke();
+ }
+
+ const lifeRatio = p.life / p.initialLife;
+ const emergenceFactor = p.isPrimordial ? (p.primordialTime / primordialDuration) : (1.0 - Math.pow(1.0 - lifeRatio, 2));
+ const finalRadius = p.isPrimordial ? currentParticleRadius : p.radius * emergenceFactor;
+ const mainParticleAlpha = emergenceFactor * particleHeadBaseAlpha;
+ const highlightAlpha = emergenceFactor * (particleHeadBaseAlpha + 0.1);
+
+ ctx.beginPath();
+ ctx.arc(p.x, p.y, Math.max(0.1, finalRadius), 0, Math.PI * 2);
+ if (!p.isPrimordial) { // Use speed-based color only after primordial phase
+ const speed = Math.hypot(p.vx, p.vy);
+ r = Math.min(255, particleColorBase[0] + speed * 15);
+ g = Math.min(255, particleColorBase[1] - speed * 5);
+ b = Math.min(255, particleColorBase[2] + speed * 5);
+ }
+ ctx.fillStyle = `rgba(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(b)}, ${mainParticleAlpha})`;
+ ctx.fill();
+
+ if (finalRadius > 0.5) {
+ ctx.beginPath();
+ ctx.arc(p.x, p.y, Math.max(0.1, finalRadius * 0.5), 0, Math.PI * 2);
+ ctx.fillStyle = `rgba(${particleColorHighlight[0]}, ${particleColorHighlight[1]}, ${particleColorHighlight[2]}, ${highlightAlpha})`;
+ ctx.fill();
+ }
+
+ p.life--;
+
+ if (p.life <= 0 || p.x < -finalRadius * 2 || p.x > canvas.width + finalRadius*2 || p.y < -finalRadius*2 || p.y > canvas.height + finalRadius*2) {
+ // Reset to primordial state when respawning
+ p.x = Math.random() * canvas.width;
+ p.y = Math.random() * canvas.height;
+ p.vx = (Math.random() - 0.5) * 0.1;
+ p.vy = (Math.random() - 0.5) * 0.1;
+ p.life = p.initialLife;
+ p.trail = [];
+ p.isPrimordial = true;
+ p.primordialTime = 0;
+ }
+ });
+ }
+
+ function animateIntro() {
+ drawIntroPattern();
+ time++;
+ if (canvas.isConnected) {
+ requestAnimationFrame(animateIntro);
+ }
+ }
+
+ if (!canvas.dataset.animationRunningIntro) {
+ canvas.dataset.animationRunningIntro = 'true';
+ animateIntro();
+ }
+ },
+
+ // Color scheme placeholder (fallback)
+ renderColorPlaceholder: function(swatchDiv, schemeName) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #333333, #666666, #999999, #cccccc)';
+
+ // Add text
+ const nameSpan = document.createElement('span');
+ nameSpan.textContent = schemeName;
+ nameSpan.style.position = 'absolute';
+ nameSpan.style.top = '50%';
+ nameSpan.style.left = '50%';
+ nameSpan.style.transform = 'translate(-50%, -50%)';
+ nameSpan.style.color = 'white';
+ nameSpan.style.textShadow = '1px 1px 1px black';
+ nameSpan.style.fontSize = '10px';
+
+ swatchDiv.appendChild(nameSpan);
+ }
+ };
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
+ // Make it async call immediately
+ (async () => { await window.NoiseVisualizer._preloadKofiImage(); })();
+ } else {
+ document.addEventListener('DOMContentLoaded', async () => {
+ await window.NoiseVisualizer._preloadKofiImage();
+ });
+ }
+}
\ No newline at end of file
diff --git a/web/shader_params_save_button.js b/web/shader_params_save_button.js
new file mode 100644
index 0000000..bbf612a
--- /dev/null
+++ b/web/shader_params_save_button.js
@@ -0,0 +1,420 @@
+// Add a button to manually save shader parameters to file
+import { app } from "../../scripts/app.js";
+
+app.registerExtension({
+ name: "ShaderParamsSaveButton",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "ShaderNoiseKSampler") {
+ // Store original onNodeCreated to maintain the node's behavior
+ const origOnNodeCreated = nodeType.prototype.onNodeCreated;
+
+ // Add our button to the node
+ nodeType.prototype.onNodeCreated = function() {
+ // Call original function to preserve existing behavior
+ if (origOnNodeCreated) {
+ origOnNodeCreated.apply(this, arguments);
+ }
+
+ // Keep track of the node instance
+ const node = this;
+
+ // Track if we're currently saving (to prevent double-clicks)
+ let isSaving = false;
+
+ // Add indicator widget that will show when parameters need saving
+ // Create a custom widget object instead of using the built-in text widget
+ const indicatorWidget = {
+ name: "โ ๏ธ Parameters with ๐ must be saved โ ๏ธ",
+ type: "custom_indicator",
+ value: "",
+ options: { className: "shader-params-indicator" },
+ tooltip: "parameters marked with ๐ require saving to take effect in the generation process",
+ disabled: true,
+ // Add computeSize method to properly handle resizing
+ computeSize: function() {
+ // Return fixed height but variable width based on parent node width
+ if (this.parent && this.parent.size) {
+ // Adjust width to match node width with some padding
+ return [this.parent.size[0] - 30, 20];
+ }
+ return [220, 28]; // Default size if parent not available
+ },
+ // Custom draw method for the widget
+ draw: function(ctx, node, widget_width, y, widget_height) {
+ if (!ctx) return;
+
+ // Draw background
+ ctx.fillStyle = "rgba(255, 119, 0, 0.1)";
+ ctx.strokeStyle = "#ff7700";
+ ctx.lineWidth = 1;
+
+ // Draw rounded rectangle for the widget background
+ const radius = 4;
+ const x = 15; // Padding from left edge
+ const width = widget_width - 30; // Subtract padding from both sides
+
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + widget_height - radius);
+ ctx.quadraticCurveTo(x + width, y + widget_height, x + width - radius, y + widget_height);
+ ctx.lineTo(x + radius, y + widget_height);
+ ctx.quadraticCurveTo(x, y + widget_height, x, y + widget_height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+
+ ctx.fill();
+ ctx.stroke();
+
+ // Draw text
+ ctx.fillStyle = "#ff7700";
+ ctx.font = "bold 12px Arial";
+ ctx.textAlign = "center";
+ ctx.fillText(this.name, x + width / 2, y + widget_height / 2 + 4);
+
+ return widget_height;
+ }
+ };
+
+ // Add widget reference to node
+ indicatorWidget.parent = this;
+ this.widgets.push(indicatorWidget);
+
+ // --- Refactored Save Function ---
+ const saveParameters = () => {
+ if (isSaving) return;
+ isSaving = true;
+
+ // Change button text to indicate saving
+ if (saveButtonWidget) saveButtonWidget.name = "Saving...";
+ console.log("Starting save process (triggered)...");
+
+ // Print each widget and its properties for deep debugging
+ if (node.widgets) {
+ console.log("Widget details:");
+ node.widgets.forEach((widget, index) => {
+ console.log(`Widget ${index}: name="${widget.name}", type=${widget.type}, value=${widget.value}`);
+ });
+ }
+
+ // Find the widgets that contain our values
+ // Default values
+ let shaderScale = 1.0;
+ let shaderOctaves = 1;
+ let shaderWarpStrength = 0.5;
+ let shaderShapeStrength = 1.0;
+ let shaderPhaseShift = 0.5;
+ let shaderColorIntensity = 0.8;
+ let shaderType = "tensor_field";
+ let shaderShapeType = "none";
+ let colorScheme = "none";
+
+ // Try to find widgets by partial name match
+ if (node.widgets) {
+ for (const widget of node.widgets) {
+ // Ensure widget and widget.name exist before accessing
+ if (!widget || typeof widget.name !== 'string') continue;
+
+ const name = widget.name.toLowerCase();
+
+ // Use partial name matching for more flexibility
+ if (name.includes("scale") && !name.includes("color")) {
+ shaderScale = widget.value;
+ console.log(`Found shader scale: ${widget.value}`);
+ }
+ else if (name.includes("octaves")) {
+ shaderOctaves = widget.value;
+ console.log(`Found octaves: ${widget.value}`);
+ }
+ else if (name.includes("warp")) {
+ shaderWarpStrength = widget.value;
+ console.log(`Found warp strength: ${widget.value}`);
+ }
+ else if (name.includes("shape") && name.includes("strength")) {
+ shaderShapeStrength = widget.value;
+ console.log(`Found shape strength: ${widget.value}`);
+ }
+ else if (name.includes("phase")) {
+ shaderPhaseShift = widget.value;
+ console.log(`Found phase: ${widget.value}`);
+ }
+ else if (name.includes("color") && name.includes("intensity")) {
+ shaderColorIntensity = widget.value;
+ console.log(`Found color intensity: ${widget.value}`);
+ }
+ else if (name.includes("shader") && name.includes("type")) {
+ shaderType = widget.value;
+ console.log(`Found shader type: ${widget.value}`);
+ }
+ else if (name.includes("shape") && name.includes("type")) {
+ shaderShapeType = widget.value;
+ console.log(`Found shape type: ${widget.value}`);
+ }
+ else if (name.includes("color") && name.includes("scheme")) {
+ colorScheme = widget.value;
+ console.log(`Found color scheme: ${widget.value}`);
+ }
+ }
+ }
+
+ // Create the properties object with the gathered values
+ const currentProps = {
+ shaderType: shaderType,
+ shaderScale: shaderScale,
+ shaderOctaves: shaderOctaves,
+ shaderWarpStrength: shaderWarpStrength,
+ shaderShapeType: shaderShapeType,
+ shaderShapeStrength: shaderShapeStrength,
+ shaderPhaseShift: shaderPhaseShift,
+ colorScheme: colorScheme,
+ shaderColorIntensity: shaderColorIntensity
+ };
+
+ console.log("Saving shader properties:", currentProps);
+
+ try {
+ // Serialize with pretty printing
+ const jsonData = JSON.stringify(currentProps, null, 2);
+
+ // Save to localStorage as well for redundancy
+ try {
+ localStorage.setItem('shader_params', jsonData);
+ console.log("Saved to localStorage successfully");
+ } catch (localErr) {
+ console.error("Failed to save to localStorage:", localErr);
+ }
+
+ // Send the file directly to the data directory - most reliable method
+ // Create a simple download link to trigger the file save
+ const blob = new Blob([jsonData], {type: 'application/json'});
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'shader_params.json';
+ document.body.appendChild(a);
+ a.click();
+
+ // Show instructions in console AFTER download is triggered
+ console.log("%cIMPORTANT: Please save the downloaded file to the following location (overwrite if exists):", "color: red; font-weight: bold");
+ console.log("%ccustom_nodes/ComfyUI-ShaderNoiseKsampler/data/shader_params.json", "color: blue; font-weight: bold");
+
+ // Clean up and update button state on success
+ setTimeout(() => {
+ try {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ console.log("Shader params JSON download link removed.");
+ } catch (cleanupError) {
+ console.warn("Could not clean up download link:", cleanupError);
+ }
+
+ // if (saveButtonWidget) saveButtonWidget.name = "โ Parameters Not Saved";
+ setTimeout(() => {
+ if (saveButtonWidget) saveButtonWidget.name = "๐พ Save Shader Parameters";
+ isSaving = false;
+ }, 2000);
+ }, 500);
+
+ } catch (error) {
+ console.error("Error saving shader parameters:", error);
+ if (saveButtonWidget) saveButtonWidget.name = "Error Saving!";
+ setTimeout(() => {
+ if (saveButtonWidget) saveButtonWidget.name = "๐พ Save Shader Parameters";
+ isSaving = false;
+ }, 3000);
+ }
+ }; // --- End of Refactored Save Function ---
+
+ // Add save button using the refactored function
+ const saveButtonWidget = this.addWidget("button", " ๐พ Save Shader Parameters", null, saveParameters);
+
+ // Extend the widget with a tooltip property that ComfyUI's system recognizes
+ saveButtonWidget.options = saveButtonWidget.options || {};
+ saveButtonWidget.options.className = "save-shader-params";
+
+ // Add tooltip following ComfyUI's pattern
+ saveButtonWidget.tooltip = "Save parameters (Alt+S) -- file must be named shader_params.json -- always overwrite old file -- [Save location: `custom_nodes/ComfyUI-ShaderNoiseKsampler/data/shader_params.json`][WIP]";
+
+ // Move both the indicator and save button widgets to the end of all widgets
+ // This ensures they appear at the bottom of all settings
+ setTimeout(() => {
+ if (this.widgets && this.widgets.length > 0) {
+ // Get the indicators's current index
+ const indicatorIndex = this.widgets.indexOf(indicatorWidget);
+ if (indicatorIndex !== -1) {
+ // Remove it from its current position
+ this.widgets.splice(indicatorIndex, 1);
+ }
+
+ // Get the save button's current index
+ const buttonIndex = this.widgets.indexOf(saveButtonWidget);
+ if (buttonIndex !== -1) {
+ // Remove it from its current position
+ this.widgets.splice(buttonIndex, 1);
+ }
+
+ // Add them back at the end, indicator first then save button
+ this.widgets.push(indicatorWidget);
+ this.widgets.push(saveButtonWidget);
+
+ // Ensure the node is redrawn to show the updated widget positions
+ this.setDirtyCanvas(true, true);
+ }
+ }, 100);
+
+ // --- Keybinding Logic ---
+ const handleKeyDown = (event) => {
+ // Check for Alt+S and if this node is currently selected
+ if (event.altKey && event.key === 's') {
+ // Check if the graph canvas and selected nodes exist
+ if (app.canvas && app.canvas.current_node) {
+ // Check if the currently selected node is this node
+ if (app.canvas.current_node === node) {
+ console.log("Alt+S detected for selected ShaderNoiseKSampler node.");
+ event.preventDefault(); // Prevent browser's default Alt+S action
+ event.stopPropagation(); // Stop event from bubbling up
+ saveParameters(); // Trigger the save function
+ }
+ } else if (app.canvas && app.canvas.selected_nodes && Object.keys(app.canvas.selected_nodes).length === 1 && app.canvas.selected_nodes[node.id]) {
+ // Fallback check for selected_nodes if current_node isn't reliable
+ console.log("Alt+S detected for selected ShaderNoiseKSampler node (using selected_nodes).");
+ event.preventDefault();
+ event.stopPropagation();
+ saveParameters();
+ }
+ }
+ };
+
+ // Attach the event listener to the document
+ document.addEventListener('keydown', handleKeyDown);
+
+ // Store handler reference for removal
+ this.handleKeyDown = handleKeyDown;
+
+ // Original onRemoved method if it exists
+ const origOnRemoved = this.onRemoved;
+
+ // Add logic to remove the event listener when the node is removed
+ this.onRemoved = function() {
+ console.log("Removing keydown listener for node:", this.id);
+ document.removeEventListener('keydown', this.handleKeyDown);
+
+ // Call original onRemoved if it existed
+ if (origOnRemoved) {
+ origOnRemoved.apply(this, arguments);
+ }
+ };
+ // --- End Keybinding Logic ---
+ };
+ }
+ },
+ // Add CSS styling for the button
+ async setup(app) {
+ // Add a small CSS rule for spacing and tooltip styling
+ const style = document.createElement("style");
+ style.textContent = `
+ .save-shader-params {
+ padding: 6px;
+ background-color: #5c5c5c;
+ color: white;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+
+ .shader-params-indicator {
+ padding: 4px;
+ font-weight: bold;
+ color: #ff7700;
+ background-color: rgba(255, 119, 0, 0.1);
+ border-left: 3px solid #ff7700;
+ border-radius: 2px;
+ text-align: center;
+ pointer-events: none;
+ user-select: none;
+ }
+
+ /* The tooltip container needs positioning */
+ .tooltip-container {
+ position: relative;
+ display: inline-block;
+ }
+
+ /* Style for ComfyUI-compatible tooltips */
+ .comfy-tooltip {
+ visibility: hidden;
+ background-color: rgba(40, 40, 40, 0.95);
+ color: #fff;
+ text-align: center;
+ padding: 8px;
+ border-radius: 6px;
+ position: absolute;
+ z-index: 1000;
+ bottom: 125%;
+ left: 50%;
+ transform: translateX(-50%);
+ white-space: nowrap;
+ font-size: 12px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
+ }
+
+ /* Show tooltip on hover */
+ .tooltip-container:hover .comfy-tooltip {
+ visibility: visible;
+ }
+ `;
+ document.head.appendChild(style);
+
+ // Hook into the app's widget drawing system to add tooltip support
+ // This is done after initial setup to ensure proper integration
+ const originalDrawNodeWidgets = LGraphCanvas.prototype.drawNodeWidgets;
+ if (originalDrawNodeWidgets) {
+ LGraphCanvas.prototype.drawNodeWidgets = function(node, pos, ctx, active_widget) {
+ // Call the original method first
+ const result = originalDrawNodeWidgets.call(this, node, pos, ctx, active_widget);
+
+ // After rendering widgets, check for our tooltip property
+ if (node && node.widgets) {
+ for (const widget of node.widgets) {
+ // If widget has our tooltip property and mouse is over it
+ if (widget === active_widget && widget.tooltip) {
+ // Get canvas position
+ const rect = this.canvas.getBoundingClientRect();
+
+ // Create tooltip if it doesn't exist yet
+ if (!widget._tooltip_elem) {
+ const tooltipContainer = document.createElement('div');
+ tooltipContainer.className = 'tooltip-container';
+
+ const tooltip = document.createElement('span');
+ tooltip.className = 'comfy-tooltip';
+ tooltip.textContent = widget.tooltip;
+
+ tooltipContainer.appendChild(tooltip);
+ document.body.appendChild(tooltipContainer);
+
+ widget._tooltip_elem = tooltipContainer;
+ }
+
+ // Position tooltip
+ if (widget._tooltip_elem) {
+ const x = pos[0] + rect.left;
+ const y = pos[1] + rect.top;
+
+ widget._tooltip_elem.style.left = x + 'px';
+ widget._tooltip_elem.style.top = y - 20 + 'px';
+ widget._tooltip_elem.style.display = 'block';
+ }
+ } else if (widget._tooltip_elem) {
+ // Hide tooltip when not hovering
+ widget._tooltip_elem.style.display = 'none';
+ }
+ }
+ }
+
+ return result;
+ };
+ }
+ }
+});
\ No newline at end of file
diff --git a/web/shader_renderer.js b/web/shader_renderer.js
new file mode 100644
index 0000000..fcfc2d4
--- /dev/null
+++ b/web/shader_renderer.js
@@ -0,0 +1,2395 @@
+// shader_renderer.js - Adds shader visualization to ShaderNoiseKSampler node
+// This file has a single responsibility: rendering shaders using a canvas approach
+
+import { app } from "../../scripts/app.js";
+
+//-------------------------------------------------------------------------
+// HELPER FUNCTIONS
+//-------------------------------------------------------------------------
+// Helper function to get color scheme name from string representation
+function getColorSchemeName(scheme) {
+ const schemeNames = {
+ "none": "Black & White",
+ "blue_red": "Blue to Red",
+ "viridis": "Viridis",
+ "plasma": "Plasma",
+ "inferno": "Inferno",
+ "magma": "Magma",
+ "turbo": "Turbo",
+ "jet": "Jet",
+ "rainbow": "Rainbow",
+ "cool": "Cool",
+ "hot": "Hot",
+ "parula": "Parula",
+ "hsv": "HSV",
+ "autumn": "Autumn",
+ "winter": "Winter",
+ "spring": "Spring",
+ "summer": "Summer",
+ "copper": "Copper",
+ "pink": "Pink",
+ "bone": "Bone",
+ "ocean": "Ocean",
+ "terrain": "Terrain",
+ "neon": "Neon",
+ "fire": "Fire"
+ };
+ return schemeNames[scheme] || scheme;
+}
+
+// Register a callback to run when ComfyUI is fully loaded
+app.registerExtension({
+ name: "ShaderNoiseKSampler.ShaderRenderer",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ // Apply only to the shader noise sampler node
+ if (nodeData.name === "ShaderNoiseKSampler") {
+ // Store the original onNodeCreated function if it exists
+ const origOnNodeCreated = nodeType.prototype.onNodeCreated;
+
+ // Add our own onNodeCreated function
+ nodeType.prototype.onNodeCreated = function() {
+ // Call the original onNodeCreated if it exists
+ if (origOnNodeCreated) {
+ origOnNodeCreated.apply(this, arguments);
+ }
+
+ // Add shader properties
+ this.properties = this.properties || {};
+ this.properties.shaderVisible = false;
+ this.properties.tooltipsVisible = true; // Add new property for tooltips visibility
+ this.properties.shaderType = "domain_warp";
+ this.properties.shaderSpeed = 0.2;
+ this.properties.shaderColorIntensity = 0.8;
+ this.properties.shaderTime = 0;
+ this.properties.lastRenderTime = 0;
+ this.properties.shaderScale = 1.0;
+ this.properties.shaderOctaves = 1;
+ this.properties.shaderShapeType = "none";
+ this.properties.shaderShapeStrength = 1.0;
+ this.properties.shaderWarpStrength = 0.5;
+ this.properties.shaderPhaseShift = 0.5;
+ this.properties.shaderFrequencyRange = 0; // 0=all, 1=low, 2=mid, 3=high, 4=custom
+ this.properties.shaderDistribution = 0; // 0=cauchy, 1=laplace
+ this.properties.shaderAdaptationStrength = 0.5;
+ this.properties.shaderResolutionScale = 512; // Medium resolution
+
+ // Store the shader height
+ this.shaderHeight = 200;
+
+ // Track animation state
+ this.animationFrameId = null;
+ this.isShaderActive = false;
+
+ // Initialize shader canvas once
+ this.initShaderCanvas();
+
+ // Custom widget for shader rendering
+ this.addWidget("toggle", "Show Shader", this.properties.shaderVisible, (v) => {
+ this.properties.shaderVisible = v;
+
+ // Update node size
+ const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
+ const baseHeight = baseSize[1];
+
+ if (v && this.gl) {
+ // If turning on, calculate shader height for current node size
+ if (!this.shaderHeight || this.shaderHeight < 50) {
+ this.shaderHeight = 200; // Default height
+ }
+
+ // Update node size to include shader
+ this.size[1] = baseHeight + this.shaderHeight;
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+
+ // Activate shader
+ this.isShaderActive = true;
+
+ // Reset render time to avoid jumps in animation
+ this.properties.lastRenderTime = 0;
+ } else {
+ // If turning off, remove shader height
+ this.size[1] = baseHeight;
+
+ // Deactivate shader
+ this.isShaderActive = false;
+
+ // Cancel any pending animation frame
+ if (this.animationFrameId) {
+ cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = null;
+ }
+ }
+
+ this.setDirtyCanvas(true, true);
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Toggle the visibility of the shader preview visualization [visual aid to help you see the shader noise pattern before it is applied for sampling]";
+
+ // Add toggle for tooltips visibility
+ this.addWidget("toggle", "Show Tooltips", this.properties.tooltipsVisible, (v) => {
+ this.properties.tooltipsVisible = v;
+
+ // Apply tooltip visibility to all widgets
+ for (let i = 0; i < this.widgets.length; i++) {
+ // Skip the Show Tooltips widget itself to always show its tooltip
+ if (this.widgets[i].name === "Show Tooltips") continue;
+
+ // Store original tooltip if not already stored
+ if (!this.widgets[i]._originalTooltip && this.widgets[i].tooltip) {
+ this.widgets[i]._originalTooltip = this.widgets[i].tooltip;
+ }
+
+ // Set or clear tooltip based on visibility setting
+ if (v && this.widgets[i]._originalTooltip) {
+ this.widgets[i].tooltip = this.widgets[i]._originalTooltip;
+ } else {
+ this.widgets[i].tooltip = "";
+ }
+ }
+
+ this.setDirtyCanvas(true, true);
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Toggle the visibility of tooltips for all shader parameters [disable if tooltips get in the way of controls]";
+
+ // Updated shader noise type combo with all the new types
+ this.addWidget("combo", "Shader Noise Type ๐", this.properties.shaderType, (v) => {
+ this.properties.shaderType = v;
+
+ // Load the shader if it's not already loaded and shader is active
+ if (this.loadShader && (this.isShaderActive || this.properties.shaderVisible)) {
+ this.loadShader(v);
+ }
+
+ this.setDirtyCanvas(true, true);
+ }, {
+ values: [
+ "domain_warp", "tensor_field", "curl_noise"
+ ]
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Select the type of shader noise pattern to use in the visualization [different types have different characteristic outputs]";
+
+ // Add shape mask type widget
+ this.addWidget("combo", "Shape Mask Type ๐", this.properties.shaderShapeType, (v) => {
+ this.properties.shaderShapeType = v;
+ this.setDirtyCanvas(true, true);
+ }, {
+ values: ["none", "radial", "linear", "spiral", "checkerboard", "spots", "hexgrid", "stripes", "gradient", "vignette", "cross", "stars", "triangles", "concentric", "rays", "zigzag"]
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Apply a shape mask to the shader noise pattern to create more complex structures [not post processing - is applied to shader noise pattern before rendering]";
+
+ // Add color scheme widget
+ this.properties.colorScheme = this.properties.colorScheme || "none";
+ this.addWidget("combo", "Color Scheme ๐", this.properties.colorScheme, (v) => {
+ this.properties.colorScheme = v;
+ this.setDirtyCanvas(true, true);
+ }, {
+ values: [
+ "none", "blue_red", "viridis", "plasma", "inferno",
+ "magma", "turbo", "jet", "rainbow", "cool",
+ "hot", "parula", "hsv", "autumn", "winter",
+ "spring", "summer", "copper", "pink", "bone",
+ "ocean", "terrain", "neon", "fire"
+ ]
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Choose a color palette to apply to the shader noise visualization [not post processing - is applied to the shader noise pattern before rendering]";
+
+ this.addWidget("slider", "Noise Scale ๐", this.properties.shaderScale, (v) => {
+ this.properties.shaderScale = v;
+ this.setDirtyCanvas(true, true);
+ }, { min: 0.1, max: 10.0, step: 0.001, precision: 3 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Adjust the scale of the shader noise pattern - lower values create larger, zoomed-in features; higher values create smaller, zoomed-out features [small value shifts can lead to larger variations]";
+
+ this.addWidget("slider", "Octaves ๐", this.properties.shaderOctaves, (v) => {
+ this.properties.shaderOctaves = v;
+ this.setDirtyCanvas(true, true);
+ }, { min: 1, max: 8, step: 0.1, precision: 1 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Number of shader noise layers to combine - higher values add more detail and complexity [small value shifts can lead to larger variations]";
+
+ this.addWidget("slider", "Warp Strength ๐", this.properties.shaderWarpStrength, (v) => {
+ this.properties.shaderWarpStrength = v;
+ this.setDirtyCanvas(true, true);
+ }, { min: 0.0, max: 5.0, step: 0.001, precision: 3 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Control how much the shader noise pattern warps and distorts - higher values create more swirling or complex transformations [small adjustments are good for subtle variations]";
+
+ this.addWidget("slider", "Shape Mask Strength ๐", this.properties.shaderShapeStrength, (v) => {
+ this.properties.shaderShapeStrength = v;
+ this.setDirtyCanvas(true, true);
+ }, { min: 0.0, max: 2.0, step: 0.0005, precision: 4 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Adjust the intensity of the shape mask's effect on the shader noise pattern - higher values make the shape more prominent [small adjustments are good for subtle variations - not effective without shape mask]";
+
+ this.addWidget("slider", "Phase Shift ๐", this.properties.shaderPhaseShift, (v) => {
+ this.properties.shaderPhaseShift = v;
+ this.setDirtyCanvas(true, true);
+ }, { min: 0.0, max: 2.0, step: 0.0005, precision: 4 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Shift the phase of the shader noise pattern to create different variations or animate patterns over time [small adjustments are good for subtle variations]";
+
+ this.addWidget("slider", "Color Intensity ๐", this.properties.shaderColorIntensity, (v) => {
+ this.properties.shaderColorIntensity = v;
+ this.setDirtyCanvas(true, true);
+ }, { min: 0.0, max: 1.0, step: 0.0005, precision: 4 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Adjust the intensity of the color scheme application - lower values are more desaturated, higher values are more vibrant [small adjustments are good for subtle variations - not effective without color scheme]";
+
+ this.addWidget("slider", "Animation Speed ๐ฅ๏ธ", this.properties.shaderSpeed, (v) => {
+ this.properties.shaderSpeed = v;
+ this.setDirtyCanvas(true, true);
+ }, { min: 0.1, max: 3.0, step: 0.001, precision: 3 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Control how quickly the shader noise pattern animates in the preview [UI effect only - not used in final image generation]";
+
+ // Add resolution quality slider
+ this.addWidget("slider", "Pixel Resolution ๐ฅ๏ธ", this.properties.shaderResolutionScale, (v) => {
+ this.properties.shaderResolutionScale = v;
+ // Trigger canvas resize to apply the new resolution
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+ this.setDirtyCanvas(true, true);
+ }, { min: 128, max: 1024, step: 1, precision: 0 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Set the resolution of the shader preview - higher values increase quality but require more processing power [UI effect only - not used in final image generation]";
+
+ // Ensure initial tooltip visibility is correctly applied and _originalTooltip is captured
+ const initialTooltipsStateIsOn = this.properties.tooltipsVisible;
+ for (let i = 0; i < this.widgets.length; i++) {
+ const widget = this.widgets[i];
+
+ if (widget.name === "Show Tooltips") {
+ // The "Show Tooltips" widget's tooltip is managed by its own definition
+ // and should always be visible.
+ continue;
+ }
+
+ // Capture the hardcoded tooltip text as _originalTooltip.
+ // This ensures _originalTooltip always reflects the intended full tooltip text.
+ if (widget.tooltip && typeof widget.tooltip === 'string' && widget.tooltip.trim() !== "") {
+ widget._originalTooltip = widget.tooltip;
+ } else {
+ widget._originalTooltip = ""; // Mark as having no original tooltip if none was defined.
+ }
+
+ // Set the currently visible tooltip based on the initial state.
+ if (initialTooltipsStateIsOn && widget._originalTooltip) {
+ widget.tooltip = widget._originalTooltip;
+ } else {
+ widget.tooltip = ""; // Hide tooltip if initially off or no original text.
+ }
+ }
+
+ // Make the node resizable
+ this.resizable = true;
+
+ // Set minimum size for the node to prevent it from becoming too small
+ this.min_height = 100; // Minimum height including the shader area
+ this.min_width = 300; // Minimum width
+
+ // Add a method to handle node resize
+ nodeType.prototype.onResize = function(size) {
+ // Set flag to indicate we're in a resize operation
+ this._isResizing = true;
+
+ // Enforce minimum size constraints
+ size[0] = Math.max(this.min_width || 300, size[0]);
+
+ // Get base height without shader area
+ const baseSize = origComputeSize ? origComputeSize.call(this, [size[0], 0]) : [size[0], 0];
+ const baseHeight = baseSize[1];
+
+ // Calculate minimum required height
+ const minTotalHeight = baseHeight + 50; // base + minimum shader height
+ size[1] = Math.max(minTotalHeight, size[1]);
+
+ // Calculate new shader height based on requested size
+ if (this.properties.shaderVisible) {
+ // Update shader height based on the requested node size
+ this.shaderHeight = Math.max(50, size[1] - baseHeight);
+
+ // Resize the shader canvas to match new dimensions
+ this.resizeShaderCanvas(size[0], this.shaderHeight);
+ }
+
+ // Update node size
+ this.size = size;
+
+ // Clear the resize flag
+ this._isResizing = false;
+
+ // Tell the system to recalculate positions
+ this.setDirtyCanvas(true, true);
+ };
+
+ // Method to resize the shader canvas
+ nodeType.prototype.resizeShaderCanvas = function(width, height) {
+ if (!this.gl || !this.shaderCanvas) return;
+
+ // Ensure minimum canvas height
+ height = Math.max(50, height);
+
+ // Calculate aspect ratio
+ const aspectRatio = width / height;
+
+ // Get target resolution (square resolution that maintains aspect ratio)
+ const targetRes = Math.round(this.properties.shaderResolutionScale);
+
+ // Calculate width and height at target resolution, maintaining aspect ratio
+ let canvasWidth, canvasHeight;
+ if (aspectRatio >= 1) {
+ // Wider than tall
+ canvasWidth = targetRes;
+ canvasHeight = Math.round(targetRes / aspectRatio);
+ } else {
+ // Taller than wide
+ canvasHeight = targetRes;
+ canvasWidth = Math.round(targetRes * aspectRatio);
+ }
+
+ // Set internal canvas resolution (actual pixels)
+ this.shaderCanvas.width = canvasWidth;
+ this.shaderCanvas.height = canvasHeight;
+
+ // Set displayed size via CSS (this is what the user sees)
+ this.shaderCanvas.style.width = width + "px";
+ this.shaderCanvas.style.height = height + "px";
+
+ // Update WebGL viewport to match internal resolution
+ this.gl.viewport(0, 0, canvasWidth, canvasHeight);
+
+ // Store the display dimensions
+ this.displayWidth = width;
+ this.displayHeight = height;
+
+ // Update shader height property to match the passed height
+ // This ensures all parts of the code are using the same value
+ this.shaderHeight = height;
+ };
+ };
+
+ // Override getExtraMenuOptions to add shader visibility toggle
+ const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
+ nodeType.prototype.getExtraMenuOptions = function(canvas, options) {
+ // Call original if it exists
+ if (origGetExtraMenuOptions) {
+ origGetExtraMenuOptions.call(this, canvas, options);
+ }
+
+ // Add toggle for shader visibility
+ const toggleShaderPreview = {
+ content: this.properties.shaderVisible ? "Hide Shader Preview" : "Show Shader Preview",
+ callback: () => {
+ // Toggle shader visibility
+ this.properties.shaderVisible = !this.properties.shaderVisible;
+
+ // Update the shader toggle widget to match
+ const shaderWidget = this.widgets?.find(w => w.name === "Show Shader");
+ if (shaderWidget) {
+ shaderWidget.value = this.properties.shaderVisible;
+ }
+
+ // Update node size
+ const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
+ const baseHeight = baseSize[1];
+
+ if (this.properties.shaderVisible && this.gl) {
+ // If turning on, calculate shader height for current node size
+ if (!this.shaderHeight || this.shaderHeight < 50) {
+ this.shaderHeight = 200; // Default height
+ }
+
+ // Update node size to include shader
+ this.size[1] = baseHeight + this.shaderHeight;
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+
+ // Activate shader
+ this.isShaderActive = true;
+
+ // Reset render time to avoid jumps in animation
+ this.properties.lastRenderTime = 0;
+ } else {
+ // If turning off, remove shader height
+ this.size[1] = baseHeight;
+
+ // Deactivate shader
+ this.isShaderActive = false;
+
+ // Cancel any pending animation frame
+ if (this.animationFrameId) {
+ cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = null;
+ }
+ }
+
+ this.setDirtyCanvas(true, true);
+ }
+ };
+
+ // Add toggle for tooltips visibility
+ const toggleTooltips = {
+ content: this.properties.tooltipsVisible ? "Hide Tooltips" : "Show Tooltips",
+ callback: () => {
+ // Toggle tooltips visibility
+ this.properties.tooltipsVisible = !this.properties.tooltipsVisible;
+
+ // Update the tooltips toggle widget to match
+ const tooltipsWidget = this.widgets?.find(w => w.name === "Show Tooltips");
+ if (tooltipsWidget) {
+ tooltipsWidget.value = this.properties.tooltipsVisible;
+
+ // Trigger the widget's callback to update all tooltips
+ if (tooltipsWidget.callback) {
+ tooltipsWidget.callback(this.properties.tooltipsVisible);
+ }
+ }
+
+ this.setDirtyCanvas(true, true);
+ }
+ };
+
+ // Add resolution quality submenu
+ const resolutionLevels = [
+ ["Very Low (128px)", 128],
+ ["Low (256px)", 256],
+ ["Medium (512px)", 512],
+ ["High (768px)", 768],
+ ["Very High (1024px)", 1024]
+ ];
+
+ const resolutionSubmenu = {
+ content: "Pixel Resolution",
+ submenu: {
+ options: resolutionLevels.map(([label, value]) => ({
+ content: label,
+ callback: () => {
+ // Update resolution quality property
+ this.properties.shaderResolutionScale = value;
+
+ // Update the resolution quality widget to match
+ const resolutionWidget = this.widgets?.find(w => w.name === "Pixel Resolution");
+ if (resolutionWidget) {
+ resolutionWidget.value = value;
+ }
+
+ // Trigger canvas resize to apply the new resolution
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+
+ // Redraw canvas
+ this.setDirtyCanvas(true, true);
+ },
+ checked: Math.abs(this.properties.shaderResolutionScale - value) < 1.0
+ }))
+ }
+ };
+
+ // Add shader noise type selection submenu
+ const shaderTypes = [
+ ["Domain Warping", "domain_warp"],
+ ["Tensor Field", "tensor_field"],
+ ["Curl Noise", "curl_noise"],
+ ];
+
+ const shaderTypeSubmenu = {
+ content: "Shader Noise Type",
+ submenu: {
+ options: shaderTypes.map(([label, type]) => ({
+ content: label,
+ callback: () => {
+ // Update shader noise type property
+ this.properties.shaderType = type;
+
+ // Update the shader noise type widget to match
+ const typeWidget = this.widgets?.find(w => w.name === "Shader Noise Type");
+ if (typeWidget) {
+ typeWidget.value = type;
+ }
+
+ // Load the shader if it's not already loaded and shader is active
+ if (this.loadShader && (this.isShaderActive || this.properties.shaderVisible)) {
+ this.loadShader(type);
+ }
+
+ // Redraw canvas
+ this.setDirtyCanvas(true, true);
+ },
+ checked: this.properties.shaderType === type
+ }))
+ }
+ };
+
+ // Add shape mask type selection submenu
+ const shapeTypes = [
+ ["None", "none", 0],
+ ["Radial", "radial", 1],
+ ["Linear", "linear", 2],
+ ["Spiral", "spiral", 3],
+ ["Checkerboard", "checkerboard", 4],
+ ["Spots", "spots", 5],
+ ["Hex Grid", "hexgrid", 6],
+ ["Stripes", "stripes", 7],
+ ["Gradient", "gradient", 8],
+ ["Vignette", "vignette", 9],
+ ["Cross", "cross", 10],
+ ["Stars", "stars", 11],
+ ["Triangles", "triangles", 12],
+ ["Concentric", "concentric", 13],
+ ["Rays", "rays", 14],
+ ["ZigZag", "zigzag", 15]
+ ];
+
+ const shapeTypeSubmenu = {
+ content: "Shape Mask Type",
+ submenu: {
+ options: shapeTypes.map(([label, type, index]) => ({
+ content: label,
+ callback: () => {
+ // Update shape mask type property
+ this.properties.shaderShapeType = type;
+
+ // Update the shape mask type widget to match
+ const shapeWidget = this.widgets?.find(w => w.name === "Shape Mask Type");
+ if (shapeWidget) {
+ shapeWidget.value = type;
+ }
+
+ // Redraw canvas
+ this.setDirtyCanvas(true, true);
+ },
+ checked: this.properties.shaderShapeType === type
+ }))
+ }
+ };
+
+ // Add color scheme selection submenu
+ const colorSchemes = [
+ ["Black & White", "none"],
+ ["Blue to Red", "blue_red"],
+ ["Viridis", "viridis"],
+ ["Plasma", "plasma"],
+ ["Inferno", "inferno"],
+ ["Magma", "magma"],
+ ["Turbo", "turbo"],
+ ["Jet", "jet"],
+ ["Rainbow", "rainbow"],
+ ["Cool", "cool"],
+ ["Hot", "hot"],
+ ["Parula", "parula"],
+ ["HSV", "hsv"],
+ ["Autumn", "autumn"],
+ ["Winter", "winter"],
+ ["Spring", "spring"],
+ ["Summer", "summer"],
+ ["Copper", "copper"],
+ ["Pink", "pink"],
+ ["Bone", "bone"],
+ ["Ocean", "ocean"],
+ ["Terrain", "terrain"],
+ ["Neon", "neon"],
+ ["Fire", "fire"]
+ ];
+
+ const colorSchemeSubmenu = {
+ content: "Color Scheme",
+ submenu: {
+ options: colorSchemes.map(([label, scheme]) => ({
+ content: label,
+ callback: () => {
+ // Update color scheme property
+ this.properties.colorScheme = scheme;
+
+ // Update the color scheme widget to match
+ const colorSchemeWidget = this.widgets?.find(w => w.name === "Color Scheme");
+ if (colorSchemeWidget) {
+ colorSchemeWidget.value = scheme;
+ }
+
+ // Redraw canvas
+ this.setDirtyCanvas(true, true);
+ },
+ checked: this.properties.colorScheme === scheme
+ }))
+ }
+ };
+
+ options.push(null, toggleShaderPreview, toggleTooltips, shaderTypeSubmenu, shapeTypeSubmenu, colorSchemeSubmenu, resolutionSubmenu);
+
+ return options;
+ };
+
+ // Override onConfigure to ensure widgets reflect loaded properties
+ const origOnConfigure = nodeType.prototype.onConfigure;
+ nodeType.prototype.onConfigure = function(info) {
+ if (origOnConfigure) {
+ origOnConfigure.apply(this, arguments);
+ }
+
+ // Ensure widgets display the correct values after loading from JSON
+ if (this.widgets && this.properties) {
+ const updateWidgetValue = (widgetName, propertyName) => {
+ const widget = this.widgets.find(w => w.name === widgetName);
+ if (widget && this.properties[propertyName] !== undefined) {
+ // Check if the value exists in the widget's options (for combos)
+ if (widget.options?.values && !widget.options.values.includes(this.properties[propertyName])) {
+ console.warn(`Value "${this.properties[propertyName]}" for property "${propertyName}" not found in options for widget "${widgetName}". Using default or first option.`);
+ // Optionally set to default or first value if invalid
+ // widget.value = widget.options.values[0];
+ } else {
+ widget.value = this.properties[propertyName];
+ }
+ }
+ };
+
+ updateWidgetValue("Show Shader", "shaderVisible");
+ updateWidgetValue("Show Tooltips", "tooltipsVisible");
+ updateWidgetValue("Shader Noise Type ๐", "shaderType");
+ updateWidgetValue("Shape Mask Type ๐", "shaderShapeType");
+ updateWidgetValue("Color Scheme ๐", "colorScheme");
+ updateWidgetValue("Noise Scale ๐", "shaderScale");
+ updateWidgetValue("Octaves ๐", "shaderOctaves");
+ updateWidgetValue("Warp Strength ๐", "shaderWarpStrength");
+ updateWidgetValue("Shape Mask Strength ๐", "shaderShapeStrength");
+ updateWidgetValue("Phase Shift ๐", "shaderPhaseShift");
+ updateWidgetValue("Animation Speed ๐ฅ๏ธ", "shaderSpeed");
+ updateWidgetValue("Color Intensity ๐", "shaderColorIntensity");
+ updateWidgetValue("Pixel Resolution ๐ฅ๏ธ", "shaderResolutionScale");
+ // Add any other widgets that need synchronization here
+
+ // After updating widget values, if the tooltips widget exists and has a callback,
+ // call it to ensure tooltip visibility is correctly applied based on the loaded state.
+ const tooltipsWidget = this.widgets.find(w => w.name === "Show Tooltips");
+ if (tooltipsWidget && tooltipsWidget.callback) {
+ // Ensure the widget.value (this.properties.tooltipsVisible) is up-to-date before calling back
+ // The updateWidgetValue call above should have handled this.
+ tooltipsWidget.callback(tooltipsWidget.value);
+ }
+ }
+
+ // If the shader should be visible based on loaded properties, ensure size is correct
+ if (this.properties?.shaderVisible) {
+ // Calculate base height
+ const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
+ const baseHeight = baseSize[1];
+
+ // Ensure shader height is valid
+ if (!this.shaderHeight || this.shaderHeight < 50) {
+ this.shaderHeight = 200; // Default height if needed
+ }
+
+ // Update node size if necessary
+ const expectedHeight = baseHeight + this.shaderHeight;
+ if (Math.abs(this.size[1] - expectedHeight) > 1) { // Use tolerance for float comparison
+ this.size[1] = expectedHeight;
+ // Also resize the canvas to match the potentially updated shaderHeight
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+ this.setDirtyCanvas(true, true);
+ }
+
+ // Ensure shader is active if it's supposed to be visible
+ this.isShaderActive = true;
+ } else {
+ // Ensure shader is inactive if not visible
+ this.isShaderActive = false;
+ }
+ };
+
+ // Initialize WebGL for shader rendering
+ nodeType.prototype.initShaderCanvas = function() {
+ // Create a canvas for shader rendering
+ this.shaderCanvas = document.createElement('canvas');
+ this.shaderCanvas.width = this.size ? this.size[0] : 250;
+ this.shaderCanvas.height = this.shaderHeight;
+
+ // Get WebGL context
+ this.gl = this.shaderCanvas.getContext('webgl');
+ if (!this.gl) {
+ console.error('WebGL not supported');
+ return;
+ }
+
+ // Set the viewport to match the canvas size
+ this.gl.viewport(0, 0, this.shaderCanvas.width, this.shaderCanvas.height);
+
+ // Setup shader programs for each shader type
+ this.shaderPrograms = {};
+
+ // Create shader programs
+ const vertexShaderSource = `
+ attribute vec2 a_position;
+ varying vec2 v_texCoord;
+
+ void main() {
+ v_texCoord = a_position * 0.5 + 0.5;
+ gl_Position = vec4(a_position, 0.0, 1.0);
+ }
+ `;
+
+ // Common fragment shader header
+ const fragmentShaderHeader = `
+ precision mediump float;
+ uniform float u_time;
+ uniform float u_intensity;
+ uniform float u_scale;
+ uniform float u_octaves;
+ uniform float u_persistence;
+ uniform float u_lacunarity;
+ uniform int u_shapeType;
+ uniform float u_shapeStrength;
+ uniform float u_warpStrength;
+ uniform float u_phaseShift;
+ uniform int u_frequencyRange;
+ uniform int u_distribution;
+ uniform float u_adaptationStrength;
+ uniform float u_resolutionScale;
+ uniform int u_colorScheme;
+ varying vec2 v_texCoord;
+
+ // Note: We no longer use resolution scaling in the shader
+ // The pixel density is now directly controlled by the canvas resolution
+
+ // Shared utility functions
+ vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
+ vec4 permute(vec4 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
+ vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
+ vec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }
+
+ float snoise(vec2 v){
+ const vec4 C = vec4(0.211324865405187, 0.366025403784439,
+ -0.577350269189626, 0.024390243902439);
+ vec2 i = floor(v + dot(v, C.yy));
+ vec2 x0 = v - i + dot(i, C.xx);
+ vec2 i1;
+ i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
+ vec4 x12 = x0.xyxy + C.xxzz;
+ x12.xy -= i1;
+ i = mod(i, 289.0);
+ vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ + i.x + vec3(0.0, i1.x, 1.0 ));
+ vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
+ dot(x12.zw,x12.zw)), 0.0);
+ m = m*m;
+ m = m*m;
+ vec3 x = 2.0 * fract(p * C.www) - 1.0;
+ vec3 h = abs(x) - 0.5;
+ vec3 ox = floor(x + 0.5);
+ vec3 a0 = x - ox;
+ m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
+ vec3 g;
+ g.x = a0.x * x0.x + h.x * x0.y;
+ g.yz = a0.yz * x12.xz + h.yz * x12.yw;
+ return 130.0 * dot(m, g);
+ }
+
+ // Random function
+ float random(vec2 st) {
+ return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453);
+ }
+
+ // Shape mask function that varies based on type
+ float applyShapeMask(vec2 st, int type) {
+ if (type == 0) { // none
+ return 1.0;
+ } else if (type == 1) { // radial
+ // Create a radial gradient from center with animation
+ vec2 center = vec2(0.5, 0.5);
+
+ // Animate center position
+ center += 0.2 * vec2(cos(u_time), sin(u_time));
+
+ float dist = distance(st, center) * 2.0;
+ return clamp(1.0 - dist, 0.0, 1.0);
+ } else if (type == 2) { // linear
+ // Create a linear gradient with animation
+ float x_offset = fract(u_time * 0.2) * 2.0;
+ float shifted_x = fract(st.x + x_offset);
+ return shifted_x;
+ } else if (type == 3) { // spiral
+ // Create a spiral pattern with animation
+ vec2 centered = st - vec2(0.5, 0.5);
+ float theta = atan(centered.y, centered.x);
+ float r = length(centered) * 2.0;
+ theta += u_time;
+ return fract((theta / (2.0 * 3.14159265) + r));
+ } else if (type == 4) { // checkerboard
+ // Create animated checkerboard
+ float grid_size = 8.0;
+ float x_offset = u_time * grid_size * 0.2;
+ float y_offset = u_time * grid_size * 0.1;
+ float x_grid = floor((st.x + x_offset / grid_size) * grid_size) * 0.5;
+ float y_grid = floor((st.y + y_offset / grid_size) * grid_size) * 0.5;
+ return mod(x_grid + y_grid, 1.0);
+ } else if (type == 5) { // spots
+ // Create animated spots
+ float mask = 0.0;
+ int num_spots = 10;
+
+ // Use deterministic pseudo-random positions based on index
+ for (int i = 0; i < 10; i++) {
+ if (i >= num_spots) break;
+ // Better randomization
+ float rand_x = fract(sin(float(i) * 78.233) * 43758.5453);
+ float rand_y = fract(sin(float(i) * 12.9898) * 43758.5453);
+ float size = fract(sin(float(i) * 93.719) * 43758.5453) * 0.3 + 0.1;
+
+ // Animate spots
+ float angle = u_time + float(i);
+ vec2 spot_pos = vec2(
+ 0.5 + cos(angle) * 0.4 * rand_x,
+ 0.5 + sin(angle) * 0.4 * rand_y
+ );
+
+ // Pulse size
+ size *= 1.0 + 0.2 * sin(u_time * 2.0 + float(i));
+
+ // Calculate spot mask
+ float dist = distance(st, spot_pos);
+ float spot_mask = clamp(1.0 - dist / size, 0.0, 1.0);
+ mask = max(mask, spot_mask);
+ }
+
+ return mask;
+ } else if (type == 6) { // hexgrid
+ // Create animated hexagonal grid
+ vec2 hex_uv = st * 6.0; // Scale for hex grid
+
+ // Apply animation
+ hex_uv.x += sin(u_time * 0.5) * 0.5;
+ hex_uv.y += cos(u_time * 0.3) * 0.5;
+
+ // Hexagon grid math
+ vec2 r = vec2(1.0, 1.73); // Hexagon ratio
+ vec2 h = r * 0.5;
+ vec2 a = mod(hex_uv, r) - h;
+ vec2 b = mod(hex_uv + h, r) - h;
+
+ // Determine distance to hexagon centers
+ float dist = min(length(a), length(b));
+
+ // Create cells with smooth borders
+ float cell_size = 0.3 + 0.1 * sin(u_time);
+ return smoothstep(cell_size + 0.05, cell_size - 0.05, dist);
+ } else if (type == 7) { // stripes
+ // Animated stripes pattern
+ float freq = 10.0;
+ float angle = 0.5 * sin(u_time * 0.2);
+
+ // Compute rotated coordinates
+ vec2 rotated = vec2(
+ st.x * cos(angle) - st.y * sin(angle),
+ st.x * sin(angle) + st.y * cos(angle)
+ );
+
+ // Animated stripe pattern
+ float stripes = sin(rotated.x * freq + u_time);
+
+ // Create binary stripes with smoothed edges
+ return smoothstep(0.0, 0.1, stripes) * smoothstep(0.0, -0.1, -stripes);
+ } else if (type == 8) { // gradient
+ // Animated moving gradient
+ float angle = u_time * 0.2;
+ vec2 dir = vec2(cos(angle), sin(angle));
+
+ // Project position onto direction vector
+ float proj = dot(st - 0.5, dir) + 0.5;
+
+ // Smooth gradient
+ return proj;
+ } else if (type == 9) { // vignette
+ // Animated vignette effect
+ vec2 center = vec2(0.5) + vec2(
+ 0.2 * sin(u_time * 0.3),
+ 0.2 * cos(u_time * 0.4)
+ );
+
+ float dist = distance(st, center);
+
+ // Animated vignette radius
+ float radius = 0.6 + 0.2 * sin(u_time * 0.5);
+ float smoothness = 0.3;
+
+ return 1.0 - smoothstep(radius - smoothness, radius, dist);
+ } else if (type == 10) { // cross
+ // Animated cross pattern
+ float thickness = 0.1 + 0.05 * sin(u_time);
+ float rotation = u_time * 0.2;
+
+ // Rotate the coordinates
+ vec2 centered = st - 0.5;
+ vec2 rotated = vec2(
+ centered.x * cos(rotation) - centered.y * sin(rotation),
+ centered.x * sin(rotation) + centered.y * cos(rotation)
+ );
+ rotated += 0.5;
+
+ // Create horizontal and vertical bars
+ float h_bar = smoothstep(0.5 - thickness, 0.5 - thickness + 0.02, rotated.y) *
+ smoothstep(0.5 + thickness, 0.5 + thickness - 0.02, rotated.y);
+ float v_bar = smoothstep(0.5 - thickness, 0.5 - thickness + 0.02, rotated.x) *
+ smoothstep(0.5 + thickness, 0.5 + thickness - 0.02, rotated.x);
+
+ return max(h_bar, v_bar);
+ } else if (type == 11) { // stars
+ // Animated star field
+ float mask = 0.0;
+ int num_stars = 20;
+
+ // Generate star field
+ for (int i = 0; i < 20; i++) {
+ if (i >= num_stars) break;
+
+ // Deterministic star positions
+ float rand_x = fract(sin(float(i) * 78.233) * 43758.5453);
+ float rand_y = fract(sin(float(i) * 12.9898) * 43758.5453);
+
+ // Star position with slow drift
+ vec2 star_pos = vec2(
+ fract(rand_x + 0.05 * sin(u_time * 0.1 + float(i))),
+ fract(rand_y + 0.05 * cos(u_time * 0.15 + float(i) * 1.5))
+ );
+
+ // Star size and brightness (twinkling)
+ float brightness = 0.5 + 0.5 * sin(u_time * (0.5 + rand_x * 0.5) + float(i));
+ float size = 0.01 + 0.015 * rand_y * brightness;
+
+ // Calculate star mask with softer edge
+ float dist = distance(st, star_pos);
+ float star_mask = smoothstep(size, size * 0.5, dist) * brightness;
+
+ // Accumulate stars
+ mask = max(mask, star_mask);
+ }
+
+ return mask;
+ } else if (type == 12) { // triangles
+ // Animated triangle pattern
+ float time = u_time * 0.2;
+ float scale = 5.0;
+
+ // Apply animation to coordinates
+ vec2 uv = st * scale;
+ uv.x += sin(time) * 0.5;
+ uv.y += cos(time * 0.7) * 0.5;
+
+ // Triangle grid
+ vec2 grid = floor(uv);
+ vec2 gv = fract(uv) - 0.5;
+
+ // Determine which half of the square we're in
+ float t = step(gv.x, gv.y);
+
+ // Calculate distance to triangle edge
+ vec2 ab = vec2(t, t);
+ vec2 bc = vec2(0.5 - gv.y, 0.5 - gv.x) * (1.0 - t) + vec2(-0.5 - gv.y, 0.5 - gv.x) * t;
+ vec2 ca = vec2(-0.5 - gv.x, -0.5 - gv.y) * (1.0 - t) + vec2(0.5 - gv.x, -0.5 - gv.y) * t;
+
+ // Minimum distance to the triangle edges
+ float d_ab = dot(gv - ab * 0.5, normalize(vec2(-ab.y, ab.x)));
+ float d_bc = dot(gv - ab - bc * 0.5, normalize(vec2(-bc.y, bc.x)));
+ float d_ca = dot(gv - ab - bc - ca * 0.5, normalize(vec2(-ca.y, ca.x)));
+
+ float d = min(min(d_ab, d_bc), d_ca);
+
+ // Create triangle pattern with pulsing border width
+ float border_width = 0.05 + 0.03 * sin(time * 1.5);
+ return smoothstep(border_width, border_width - 0.02, abs(d));
+ } else if (type == 13) { // concentric
+ // Animated concentric circles
+ vec2 center = vec2(0.5) + vec2(
+ 0.2 * sin(u_time * 0.3),
+ 0.2 * cos(u_time * 0.4)
+ );
+
+ float dist = distance(st, center);
+
+ // Animated frequency and phase
+ float freq = 10.0 + 5.0 * sin(u_time * 0.1);
+ float phase = u_time * 0.5;
+
+ // Create concentric rings
+ float rings = sin(dist * freq + phase);
+
+ // Create binary rings with smoothed edges
+ return smoothstep(0.0, 0.1, rings) * smoothstep(0.0, -0.1, -rings);
+ } else if (type == 14) { // rays
+ // Animated rays from center
+ vec2 center = vec2(0.5) + vec2(
+ 0.1 * sin(u_time * 0.3),
+ 0.1 * cos(u_time * 0.4)
+ );
+
+ vec2 toCenter = st - center;
+ float angle = atan(toCenter.y, toCenter.x);
+
+ // Animated frequency and phase for rays
+ float freq = 8.0;
+ float phase = u_time * 0.5;
+
+ // Create rays with smooth transitions
+ float rays = sin(angle * freq + phase);
+
+ // Create binary rays with smoothed edges and distance falloff
+ float dist = length(toCenter);
+ float falloff = 1.0 - smoothstep(0.0, 0.8, dist);
+
+ return smoothstep(0.0, 0.3, rays) * falloff;
+ } else if (type == 15) { // zigzag
+ // Animated zigzag pattern
+ float freq = 10.0;
+ float angle = 0.5 * sin(u_time * 0.2);
+
+ // Compute rotated coordinates
+ vec2 rotated = vec2(
+ st.x * cos(angle) - st.y * sin(angle),
+ st.x * sin(angle) + st.y * cos(angle)
+ );
+
+ // Create two perpendicular triangle waves
+ float zigzag1 = abs(2.0 * fract(rotated.x * freq - u_time * 0.5) - 1.0);
+ float zigzag2 = abs(2.0 * fract(rotated.y * freq + u_time * 0.3) - 1.0);
+
+ // Combine zigzag patterns
+ float zigzag = min(zigzag1, zigzag2);
+
+ // Create crisp zigzag lines with varying thickness
+ float thickness = 0.3 + 0.1 * sin(u_time);
+ return step(thickness, zigzag);
+ }
+
+ return 1.0; // Fallback
+ }
+
+ // Color mapping function based on the selected color scheme
+ vec3 getColor(float t, int colorScheme) {
+ // Map t from [-1, 1] to [0, 1] for color mapping
+ float normalized = (t + 1.0) * 0.5;
+
+ vec3 color;
+
+ // Switch based on color scheme
+ if (colorScheme == 0) { // none (Black & White)
+ return vec3(normalized);
+ }
+ else if (colorScheme == 1) { // blue_red
+ return mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), normalized);
+ }
+ else if (colorScheme == 2) { // viridis
+ vec3 c0 = vec3(0.267, 0.005, 0.329); // #440154
+ vec3 c1 = vec3(0.188, 0.407, 0.553); // #30678D
+ vec3 c2 = vec3(0.208, 0.718, 0.471); // #35B778
+ vec3 c3 = vec3(0.992, 0.906, 0.143); // #FDE724
+
+ if (normalized < 0.33) {
+ return mix(c0, c1, normalized * 3.0);
+ } else if (normalized < 0.66) {
+ return mix(c1, c2, (normalized - 0.33) * 3.0);
+ } else {
+ return mix(c2, c3, (normalized - 0.66) * 3.0);
+ }
+ }
+ else if (colorScheme == 3) { // plasma
+ vec3 c0 = vec3(0.050, 0.031, 0.529); // #0D0887
+ vec3 c1 = vec3(0.494, 0.012, 0.659); // #7E03A8
+ vec3 c2 = vec3(0.800, 0.275, 0.471); // #CC4678
+ vec3 c3 = vec3(0.973, 0.584, 0.255); // #F89441
+ vec3 c4 = vec3(0.941, 0.973, 0.129); // #F0F921
+
+ if (normalized < 0.25) {
+ return mix(c0, c1, normalized * 4.0);
+ } else if (normalized < 0.5) {
+ return mix(c1, c2, (normalized - 0.25) * 4.0);
+ } else if (normalized < 0.75) {
+ return mix(c2, c3, (normalized - 0.5) * 4.0);
+ } else {
+ return mix(c3, c4, (normalized - 0.75) * 4.0);
+ }
+ }
+ else if (colorScheme == 4) { // inferno
+ vec3 c0 = vec3(0.001, 0.001, 0.016); // #000004
+ vec3 c1 = vec3(0.259, 0.039, 0.408); // #420A68
+ vec3 c2 = vec3(0.576, 0.149, 0.404); // #932667
+ vec3 c3 = vec3(0.867, 0.318, 0.227); // #DD513A
+ vec3 c4 = vec3(0.988, 0.647, 0.039); // #FCA50A
+ vec3 c5 = vec3(0.988, 1.000, 0.643); // #FCFFA4
+
+ if (normalized < 0.2) {
+ return mix(c0, c1, normalized * 5.0);
+ } else if (normalized < 0.4) {
+ return mix(c1, c2, (normalized - 0.2) * 5.0);
+ } else if (normalized < 0.6) {
+ return mix(c2, c3, (normalized - 0.4) * 5.0);
+ } else if (normalized < 0.8) {
+ return mix(c3, c4, (normalized - 0.6) * 5.0);
+ } else {
+ return mix(c4, c5, (normalized - 0.8) * 5.0);
+ }
+ }
+ else if (colorScheme == 5) { // magma
+ vec3 c0 = vec3(0.001, 0.001, 0.016); // #000004
+ vec3 c1 = vec3(0.231, 0.059, 0.439); // #3B0F70
+ vec3 c2 = vec3(0.549, 0.161, 0.506); // #8C2981
+ vec3 c3 = vec3(0.871, 0.288, 0.408); // #DE4968
+ vec3 c4 = vec3(0.996, 0.624, 0.427); // #FE9F6D
+ vec3 c5 = vec3(0.988, 0.992, 0.749); // #FCFDBF
+
+ if (normalized < 0.2) {
+ return mix(c0, c1, normalized * 5.0);
+ } else if (normalized < 0.4) {
+ return mix(c1, c2, (normalized - 0.2) * 5.0);
+ } else if (normalized < 0.6) {
+ return mix(c2, c3, (normalized - 0.4) * 5.0);
+ } else if (normalized < 0.8) {
+ return mix(c3, c4, (normalized - 0.6) * 5.0);
+ } else {
+ return mix(c4, c5, (normalized - 0.8) * 5.0);
+ }
+ }
+ else if (colorScheme == 6) { // turbo
+ vec3 c0 = vec3(0.188, 0.071, 0.235); // #30123b
+ vec3 c1 = vec3(0.275, 0.408, 0.859); // #4669db
+ vec3 c2 = vec3(0.149, 0.749, 0.549); // #26bf8c
+ vec3 c3 = vec3(0.831, 1.000, 0.314); // #d4ff50
+ vec3 c4 = vec3(0.980, 0.718, 0.298); // #fab74c
+ vec3 c5 = vec3(0.729, 0.004, 0.000); // #ba0100
+
+ if (normalized < 0.2) {
+ return mix(c0, c1, normalized * 5.0);
+ } else if (normalized < 0.4) {
+ return mix(c1, c2, (normalized - 0.2) * 5.0);
+ } else if (normalized < 0.6) {
+ return mix(c2, c3, (normalized - 0.4) * 5.0);
+ } else if (normalized < 0.8) {
+ return mix(c3, c4, (normalized - 0.6) * 5.0);
+ } else {
+ return mix(c4, c5, (normalized - 0.8) * 5.0);
+ }
+ }
+ else if (colorScheme == 7) { // jet
+ vec3 c0 = vec3(0.000, 0.000, 0.498); // #00007f
+ vec3 c1 = vec3(0.000, 0.000, 1.000); // #0000ff
+ vec3 c2 = vec3(0.000, 1.000, 1.000); // #00ffff
+ vec3 c3 = vec3(1.000, 1.000, 0.000); // #ffff00
+ vec3 c4 = vec3(1.000, 0.000, 0.000); // #ff0000
+ vec3 c5 = vec3(0.498, 0.000, 0.000); // #7f0000
+
+ if (normalized < 0.2) {
+ return mix(c0, c1, normalized * 5.0);
+ } else if (normalized < 0.4) {
+ return mix(c1, c2, (normalized - 0.2) * 5.0);
+ } else if (normalized < 0.6) {
+ return mix(c2, c3, (normalized - 0.4) * 5.0);
+ } else if (normalized < 0.8) {
+ return mix(c3, c4, (normalized - 0.6) * 5.0);
+ } else {
+ return mix(c4, c5, (normalized - 0.8) * 5.0);
+ }
+ }
+ else if (colorScheme == 8) { // rainbow
+ vec3 c0 = vec3(0.431, 0.251, 0.667); // #6e40aa
+ vec3 c1 = vec3(0.075, 0.600, 0.851); // #1399d9
+ vec3 c2 = vec3(0.122, 0.745, 0.243); // #1fbe3e
+ vec3 c3 = vec3(0.816, 0.757, 0.004); // #d0c101
+ vec3 c4 = vec3(0.694, 0.027, 0.478); // #b1077a
+
+ if (normalized < 0.25) {
+ return mix(c0, c1, normalized * 4.0);
+ } else if (normalized < 0.5) {
+ return mix(c1, c2, (normalized - 0.25) * 4.0);
+ } else if (normalized < 0.75) {
+ return mix(c2, c3, (normalized - 0.5) * 4.0);
+ } else {
+ return mix(c3, c4, (normalized - 0.75) * 4.0);
+ }
+ }
+ else if (colorScheme == 9) { // cool
+ vec3 c0 = vec3(0.000, 1.000, 1.000); // #00ffff
+ vec3 c1 = vec3(1.000, 0.000, 1.000); // #ff00ff
+
+ return mix(c0, c1, normalized);
+ }
+ else if (colorScheme == 10) { // hot
+ vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
+ vec3 c1 = vec3(1.000, 0.000, 0.000); // #ff0000
+ vec3 c2 = vec3(1.000, 1.000, 0.000); // #ffff00
+ vec3 c3 = vec3(1.000, 1.000, 1.000); // #ffffff
+
+ if (normalized < 0.33) {
+ return mix(c0, c1, normalized * 3.0);
+ } else if (normalized < 0.66) {
+ return mix(c1, c2, (normalized - 0.33) * 3.0);
+ } else {
+ return mix(c2, c3, (normalized - 0.66) * 3.0);
+ }
+ }
+ else if (colorScheme == 11) { // parula
+ vec3 c0 = vec3(0.208, 0.165, 0.529); // #352a87
+ vec3 c1 = vec3(0.059, 0.361, 0.867); // #0f5cdd
+ vec3 c2 = vec3(0.000, 0.710, 0.651); // #00b5a6
+ vec3 c3 = vec3(1.000, 0.765, 0.216); // #ffc337
+ vec3 c4 = vec3(0.988, 0.996, 0.643); // #fcfea4
+
+ if (normalized < 0.25) {
+ return mix(c0, c1, normalized * 4.0);
+ } else if (normalized < 0.5) {
+ return mix(c1, c2, (normalized - 0.25) * 4.0);
+ } else if (normalized < 0.75) {
+ return mix(c2, c3, (normalized - 0.5) * 4.0);
+ } else {
+ return mix(c3, c4, (normalized - 0.75) * 4.0);
+ }
+ }
+ else if (colorScheme == 12) { // hsv
+ // HSV color wheel implemented directly
+ float h = normalized * 6.0;
+ int i = int(floor(h));
+ float f = h - float(i);
+
+ float v = 1.0;
+ float s = 1.0;
+ float p = v * (1.0 - s);
+ float q = v * (1.0 - s * f);
+ float t = v * (1.0 - s * (1.0 - f));
+
+ if (i == 0) return vec3(v, t, p);
+ else if (i == 1) return vec3(q, v, p);
+ else if (i == 2) return vec3(p, v, t);
+ else if (i == 3) return vec3(p, q, v);
+ else if (i == 4) return vec3(t, p, v);
+ else return vec3(v, p, q);
+ }
+ else if (colorScheme == 13) { // autumn
+ vec3 c0 = vec3(1.000, 0.000, 0.000); // #ff0000
+ vec3 c1 = vec3(1.000, 1.000, 0.000); // #ffff00
+
+ return mix(c0, c1, normalized);
+ }
+ else if (colorScheme == 14) { // winter
+ vec3 c0 = vec3(0.000, 0.000, 1.000); // #0000ff
+ vec3 c1 = vec3(0.000, 1.000, 1.000); // #00ffff
+
+ return mix(c0, c1, normalized);
+ }
+ else if (colorScheme == 15) { // spring
+ vec3 c0 = vec3(1.000, 0.000, 1.000); // #ff00ff
+ vec3 c1 = vec3(1.000, 1.000, 0.000); // #ffff00
+
+ return mix(c0, c1, normalized);
+ }
+ else if (colorScheme == 16) { // summer
+ vec3 c0 = vec3(0.000, 0.502, 0.400); // #008066
+ vec3 c1 = vec3(1.000, 1.000, 0.400); // #ffff66
+
+ return mix(c0, c1, normalized);
+ }
+ else if (colorScheme == 17) { // copper
+ vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
+ vec3 c1 = vec3(1.000, 0.600, 0.400); // #ff9966
+
+ return mix(c0, c1, normalized);
+ }
+ else if (colorScheme == 18) { // pink
+ vec3 c0 = vec3(0.051, 0.051, 0.051); // #0d0d0d
+ vec3 c1 = vec3(1.000, 0.000, 1.000); // #ff00ff
+ vec3 c2 = vec3(1.000, 1.000, 1.000); // #ffffff
+
+ if (normalized < 0.5) {
+ return mix(c0, c1, normalized * 2.0);
+ } else {
+ return mix(c1, c2, (normalized - 0.5) * 2.0);
+ }
+ }
+ else if (colorScheme == 19) { // bone
+ vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
+ vec3 c1 = vec3(0.329, 0.329, 0.455); // #545474
+ vec3 c2 = vec3(0.627, 0.757, 0.757); // #a0c1c1
+ vec3 c3 = vec3(1.000, 1.000, 1.000); // #ffffff
+
+ if (normalized < 0.33) {
+ return mix(c0, c1, normalized * 3.0);
+ } else if (normalized < 0.66) {
+ return mix(c1, c2, (normalized - 0.33) * 3.0);
+ } else {
+ return mix(c2, c3, (normalized - 0.66) * 3.0);
+ }
+ }
+ else if (colorScheme == 20) { // ocean
+ vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
+ vec3 c1 = vec3(0.000, 0.000, 0.600); // #000099
+ vec3 c2 = vec3(0.000, 0.600, 1.000); // #0099ff
+ vec3 c3 = vec3(0.600, 1.000, 1.000); // #99ffff
+
+ if (normalized < 0.33) {
+ return mix(c0, c1, normalized * 3.0);
+ } else if (normalized < 0.66) {
+ return mix(c1, c2, (normalized - 0.33) * 3.0);
+ } else {
+ return mix(c2, c3, (normalized - 0.66) * 3.0);
+ }
+ }
+ else if (colorScheme == 21) { // terrain
+ vec3 c0 = vec3(0.200, 0.200, 0.600); // #333399
+ vec3 c1 = vec3(0.000, 0.800, 0.400); // #00cc66
+ vec3 c2 = vec3(1.000, 0.800, 0.000); // #ffcc00
+ vec3 c3 = vec3(1.000, 1.000, 1.000); // #ffffff
+
+ if (normalized < 0.33) {
+ return mix(c0, c1, normalized * 3.0);
+ } else if (normalized < 0.66) {
+ return mix(c1, c2, (normalized - 0.33) * 3.0);
+ } else {
+ return mix(c2, c3, (normalized - 0.66) * 3.0);
+ }
+ }
+ else if (colorScheme == 22) { // neon
+ vec3 c0 = vec3(1.000, 0.000, 1.000); // #ff00ff
+ vec3 c1 = vec3(0.000, 1.000, 1.000); // #00ffff
+ vec3 c2 = vec3(1.000, 1.000, 0.000); // #ffff00
+
+ if (normalized < 0.5) {
+ return mix(c0, c1, normalized * 2.0);
+ } else {
+ return mix(c1, c2, (normalized - 0.5) * 2.0);
+ }
+ }
+ else if (colorScheme == 23) { // fire
+ vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
+ vec3 c1 = vec3(1.000, 0.000, 0.000); // #ff0000
+ vec3 c2 = vec3(1.000, 1.000, 0.000); // #ffff00
+ vec3 c3 = vec3(1.000, 1.000, 1.000); // #ffffff
+
+ if (normalized < 0.33) {
+ return mix(c0, c1, normalized * 3.0);
+ } else if (normalized < 0.66) {
+ return mix(c1, c2, (normalized - 0.33) * 3.0);
+ } else {
+ return mix(c2, c3, (normalized - 0.66) * 3.0);
+ }
+ }
+ else {
+ // Default to blue-red gradient
+ return mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), normalized);
+ }
+ }
+ `;
+
+ // Common fragment shader footer
+ const fragmentShaderFooter = `
+ void main() {
+ // Generate noise
+ float noise = fbm(v_texCoord);
+
+ // Apply shape mask if enabled
+ float shape_mask = applyShapeMask(v_texCoord, u_shapeType);
+ noise = noise * (shape_mask * u_shapeStrength + (1.0 - u_shapeStrength));
+
+ // Map to [0,1] range for color
+ vec3 color = getColor(noise, u_colorScheme);
+
+ gl_FragColor = vec4(color * u_intensity, 1.0);
+ }
+ `;
+
+ // Domain Warp shader
+ const domainWarpShader = `
+ // More precise domain warping implementation
+ float fbm_base(vec2 p) {
+ // Standard FBM using user-controlled octaves
+ float sum = 0.0;
+ float amp = 1.0;
+ float freq = 1.0;
+
+ for (int i = 0; i < 8; i++) {
+ if (float(i) >= u_octaves) break;
+ sum += amp * snoise(p * freq);
+ freq *= 2.0;
+ amp *= 0.5;
+ }
+
+ return sum;
+ }
+
+ // Improved domain warping with different warp types
+ float domainWarp(vec2 p, int warpType) {
+ // Note: Resolution is now controlled by canvas size
+
+ // Base unwarped coordinates scaled by user scale parameter
+ vec2 p0 = p * u_scale;
+
+ // Basic domain warp using noise as a displacement vector field
+ if (warpType == 0) {
+ // Create distortion vector
+ float angle = u_time * 0.1;
+ vec2 d = vec2(cos(angle), sin(angle));
+
+ // Generate the warp field
+ float warpNoise1 = snoise(p0 * 0.5);
+ float warpNoise2 = snoise(p0 * 0.5 + vec2(5.2, 1.3));
+
+ // Create the warp displacement
+ vec2 warpVec = vec2(warpNoise1, warpNoise2) * u_warpStrength;
+
+ // Apply the warp
+ vec2 warped = p0 + warpVec;
+
+ // Sample the base pattern with warped coordinates
+ return snoise(warped);
+ }
+ // Fractal domain warp (warp the warp)
+ else if (warpType == 1) {
+ // Apply progressive warping with multiple layers
+ vec2 warped = p0;
+
+ // Progressive warp layers
+ for (int i = 0; i < 3; i++) {
+ // Scale decreases for each iteration
+ float warpScale = 1.0 / pow(2.0, float(i));
+
+ // Generate warp vectors
+ float warpNoise1 = snoise(warped * warpScale + vec2(u_time * 0.05, 0.0));
+ float warpNoise2 = snoise(warped * warpScale + vec2(0.0, u_time * 0.05) + vec2(43.13, 17.21));
+
+ // Apply warp with decreasing strength for each iteration
+ float warpFactor = u_warpStrength * warpScale;
+ warped += vec2(warpNoise1, warpNoise2) * warpFactor;
+ }
+
+ // Sample final fbm with fully warped coordinates
+ return fbm_base(warped);
+ }
+ // Advanced vector field warping
+ else if (warpType == 2) {
+ // Use a separate noise function to determine flow direction
+ float flowNoise = snoise(p0 * 0.2 + vec2(u_time * 0.1, 0.0));
+ float flowAngle = flowNoise * 6.28318530718; // Map to full rotation
+
+ // Create flow direction vector
+ vec2 flowDir = vec2(cos(flowAngle), sin(flowAngle));
+
+ // Apply directional warp
+ vec2 warped = p0 + flowDir * u_warpStrength * snoise(p0 * 0.4);
+
+ // Add secondary orthogonal flow
+ vec2 perpDir = vec2(-flowDir.y, flowDir.x); // Perpendicular vector
+ warped += perpDir * u_warpStrength * 0.5 * snoise(p0 * 0.3 + vec2(10.0, 20.0));
+
+ return fbm_base(warped);
+ }
+ // Swirl warp
+ else {
+ // Calculate distance from center
+ vec2 centered = p0 - 0.5;
+ float dist = length(centered);
+
+ // Calculate angle based on distance
+ float angle = u_time + dist * u_warpStrength * 10.0;
+
+ // Create rotation matrix
+ float s = sin(angle);
+ float c = cos(angle);
+ mat2 rot = mat2(c, -s, s, c);
+
+ // Apply rotational warping
+ vec2 warped = rot * centered + 0.5;
+
+ return fbm_base(warped);
+ }
+ }
+
+ float fbm(vec2 p) {
+ int warpType = int(mod(u_octaves, 4.0));
+ float result = domainWarp(p, warpType);
+
+ // Apply user's phase shift to control contrast and distribution
+ float contrast = 1.0 + u_phaseShift;
+ result *= contrast;
+
+ // Ensure output is in valid [-1,1] range
+ return clamp(result, -1.0, 1.0);
+ }
+ `;
+
+ // Tensor Field shader
+ const tensorFieldShader = `
+ // Improved tensor field implementation with better mathematical representation
+
+ // Helper function for computing tensor eigenvectors and eigenvalues
+ void computeTensorProperties(vec2 p, out float magnitude1, out float magnitude2,
+ out vec2 direction1, out vec2 direction2) {
+ // Note: Resolution is now controlled by canvas size
+
+ // Generate a tensor field using noise gradients
+ vec2 offset = vec2(u_time * 0.05);
+ vec2 p1 = p * u_scale + offset;
+
+ // Apply warp to coordinates if warp strength is non-zero
+ if (u_warpStrength > 0.0) {
+ // Generate warp field based on noise
+ float warpNoise1 = snoise(p1 * 0.3 + vec2(0.0, 1.0));
+ float warpNoise2 = snoise(p1 * 0.3 + vec2(1.0, 0.0));
+
+ // Apply warp to coordinates
+ p1 += vec2(warpNoise1, warpNoise2) * u_warpStrength;
+ }
+
+ // Use noise derivatives to generate tensor field
+ float eps = 0.01;
+
+ // Compute approximate derivatives of noise field
+ float n00 = snoise(p1);
+ float n10 = snoise(p1 + vec2(eps, 0.0));
+ float n01 = snoise(p1 + vec2(0.0, eps));
+ float n11 = snoise(p1 + vec2(eps, eps));
+
+ // Calculate derivatives (gradient components)
+ float dx = (n10 - n00) / eps;
+ float dy = (n01 - n00) / eps;
+
+ // Second order derivatives for tensor components
+ float dxx = (n10 - 2.0 * n00 + snoise(p1 - vec2(eps, 0.0))) / (eps * eps);
+ float dyy = (n01 - 2.0 * n00 + snoise(p1 - vec2(0.0, eps))) / (eps * eps);
+ float dxy = (n11 - n10 - n01 + n00) / (eps * eps);
+
+ // Construct tensor matrix components
+ float T00 = dxx;
+ float T01 = dxy;
+ float T10 = dxy;
+ float T11 = dyy;
+
+ // Calculate eigenvalues
+ float trace = T00 + T11;
+ float det = T00 * T11 - T01 * T10;
+ float discriminant = sqrt(trace * trace - 4.0 * det);
+
+ // Two eigenvalues
+ magnitude1 = (trace + discriminant) * 0.5;
+ magnitude2 = (trace - discriminant) * 0.5;
+
+ // Calculate first eigenvector
+ if (abs(T01) > 0.0001) {
+ direction1 = normalize(vec2(T01, magnitude1 - T00));
+ } else if (abs(T10) > 0.0001) {
+ direction1 = normalize(vec2(magnitude1 - T11, T10));
+ } else {
+ // Diagonal tensor
+ direction1 = vec2(1.0, 0.0);
+ }
+
+ // Second eigenvector is perpendicular to first
+ direction2 = vec2(-direction1.y, direction1.x);
+ }
+
+ // Improved tensor field visualization
+ float tensorField(vec2 p) {
+ // Calculate tensor field components
+ float lambda1, lambda2;
+ vec2 v1, v2;
+ computeTensorProperties(p, lambda1, lambda2, v1, v2);
+
+ // Choose visualization based on octaves
+ int visualizationType = int(mod(u_octaves, 4.0));
+
+ // Different visualization modes
+ if (visualizationType == 0) {
+ // Eigenvalue visualization - shows magnitude of deformation
+ float maxEig = max(abs(lambda1), abs(lambda2));
+ return clamp(maxEig, -1.0, 1.0);
+ }
+ else if (visualizationType == 1) {
+ // Eigenvector streamlines - shows direction of principal stress
+
+ // Direction-based visualization with animating flow
+ float lineWidth = 0.08;
+ vec2 st = p;
+
+ // Calculate distance to streamline along first eigenvector
+ float flowPhase = u_time * 0.2;
+ float t = st.x * v1.x + st.y * v1.y;
+ float streamline1 = abs(fract(t * 5.0 + flowPhase) - 0.5) * 2.0;
+
+ // Calculate distance to streamline along second eigenvector
+ t = st.x * v2.x + st.y * v2.y;
+ float streamline2 = abs(fract(t * 5.0 - flowPhase) - 0.5) * 2.0;
+
+ // Combine streamlines
+ float pattern = min(streamline1, streamline2);
+ return 1.0 - smoothstep(0.0, lineWidth, pattern) * 2.0;
+ }
+ else if (visualizationType == 2) {
+ // Hyperstreamlines - thickness varies with eigenvalue magnitude
+
+ vec2 dir1 = v1 * sign(lambda1);
+ vec2 dir2 = v2 * sign(lambda2);
+
+ float weight1 = abs(lambda1) / (abs(lambda1) + abs(lambda2) + 0.001);
+ float weight2 = 1.0 - weight1;
+
+ float angle1 = atan(dir1.y, dir1.x);
+ float angle2 = atan(dir2.y, dir2.x);
+
+ float t1 = cos(5.0 * (p.x * dir1.x + p.y * dir1.y) + u_time);
+ float t2 = cos(5.0 * (p.x * dir2.x + p.y * dir2.y) - u_time);
+
+ return (t1 * weight1 + t2 * weight2) * 0.5;
+ }
+ else {
+ // Tensor ellipses
+
+ // Create an elliptical pattern aligned with eigenvectors and scaled by eigenvalues
+ vec2 centered = p - floor(p * 4.0 + 0.5) / 4.0; // Create grid
+
+ // Transform point to eigenvector basis
+ float x = dot(centered, v1);
+ float y = dot(centered, v2);
+
+ // Scale by eigenvalues (normalized to prevent distortion)
+ float maxEig = max(abs(lambda1), abs(lambda2)) + 0.1;
+ float scaledX = x * abs(lambda1) / maxEig;
+ float scaledY = y * abs(lambda2) / maxEig;
+
+ // Create ellipse
+ float ellipse = length(vec2(scaledX, scaledY));
+ float radius = 0.05;
+
+ // Animate pulsing ellipses
+ radius *= 1.0 + 0.3 * sin(u_time * 2.0);
+
+ // Return elliptical pattern
+ return 1.0 - smoothstep(0.0, 0.01, ellipse - radius) * 2.0;
+ }
+ }
+
+ float fbm(vec2 p) {
+ float result = tensorField(p);
+
+ // Apply user's phase shift to control contrast and distribution
+ float contrast = 1.0 + u_phaseShift;
+ result *= contrast;
+
+ // Ensure output is in valid [-1,1] range
+ return clamp(result, -1.0, 1.0);
+ }
+ `;
+
+ // Curl Noise shader
+ const curlNoiseShader = `
+ // Compute gradient of scalar field
+ vec2 computeGradient(vec2 p, float epsilon) {
+ // Sample the potential field at nearby points
+ float dx = snoise(vec2(p.x + epsilon, p.y)) - snoise(vec2(p.x - epsilon, p.y));
+ float dy = snoise(vec2(p.x, p.y + epsilon)) - snoise(vec2(p.x, p.y - epsilon));
+
+ // Normalize by epsilon and return
+ return vec2(dx, dy) / (2.0 * epsilon);
+ }
+
+ // Compute curl of vector field (z component in 2D)
+ float computeCurl(vec2 p, float epsilon) {
+ // For 2D curl, we need partial derivatives of two potential fields
+ // We'll use two offset perlin noise functions as our potential fields
+
+ // Sample two potential fields (offset for independence)
+ float pot1_dx = snoise(vec2(p.x + epsilon, p.y)) - snoise(vec2(p.x - epsilon, p.y));
+ float pot1_dy = snoise(vec2(p.x, p.y + epsilon)) - snoise(vec2(p.x, p.y - epsilon));
+
+ float pot2_dx = snoise(vec2(p.x + epsilon, p.y + 100.0)) - snoise(vec2(p.x - epsilon, p.y + 100.0));
+ float pot2_dy = snoise(vec2(p.x, p.y + epsilon + 100.0)) - snoise(vec2(p.x, p.y - epsilon + 100.0));
+
+ // Normalize gradients
+ pot1_dx /= (2.0 * epsilon);
+ pot1_dy /= (2.0 * epsilon);
+ pot2_dx /= (2.0 * epsilon);
+ pot2_dy /= (2.0 * epsilon);
+
+ // Compute curl (cross product in 2D: โpot2/โx - โpot1/โy)
+ return pot2_dx - pot1_dy;
+ }
+
+ // Get a fluid velocity field based on curl
+ vec2 getVelocityField(vec2 p, float time) {
+ // Use multiple frequencies for more detailed flow
+ vec2 velocity = vec2(0.0);
+ float epsilon = 0.01;
+
+ // Base frequency
+ float frequency = 1.0;
+ float amplitude = 1.0;
+
+ for (int i = 0; i < 3; i++) {
+ if (float(i) >= u_octaves) break;
+
+ // Time-varied position
+ vec2 pos = p * frequency + vec2(time * 0.1 * frequency);
+
+ // Compute curl at this frequency
+ float curl = computeCurl(pos, epsilon);
+
+ // Use curl to derive a velocity field
+ // The gradient of the curl gives us a divergence-free field
+ vec2 vel = vec2(
+ computeCurl(pos + vec2(0.0, epsilon), epsilon) - curl,
+ curl - computeCurl(pos + vec2(epsilon, 0.0), epsilon)
+ ) / epsilon;
+
+ // Add to total velocity
+ velocity += vel * amplitude;
+
+ // Prepare for next octave
+ frequency *= 2.0;
+ amplitude *= 0.5;
+ epsilon *= 0.5; // Adjust epsilon for higher frequencies
+ }
+
+ return velocity;
+ }
+
+ // Advect a property along the velocity field
+ float advect(vec2 p, vec2 velocity, float time, float dt) {
+ // Trace particle backward in time
+ vec2 particlePos = p - velocity * dt;
+
+ // Sample different noise patterns based on octave setting
+ int patternType = int(mod(u_octaves, 4.0));
+ float result;
+
+ if (patternType == 0) {
+ // Classic advected noise
+ result = snoise(particlePos * u_scale + vec2(time * 0.2));
+ }
+ else if (patternType == 1) {
+ // Dye injection visualization
+ float dist = length(fract(particlePos) - 0.5) * 2.0;
+ float spots = smoothstep(0.4, 0.0, dist);
+ result = spots * 2.0 - 1.0; // Remap to [-1, 1]
+ }
+ else if (patternType == 2) {
+ // Flow lines visualization
+ float stream = sin(dot(particlePos, normalize(velocity)) * 10.0 + time);
+ result = stream;
+ }
+ else {
+ // Vorticity visualization (shows rotations in the flow)
+ float vorticity = computeCurl(particlePos * u_scale, 0.01);
+ result = vorticity * 2.0; // Amplify for better visibility
+ }
+
+ return result;
+ }
+
+ // Apply the warp control to intensify curl
+ vec2 applyWarpIntensity(vec2 velocity, float warp) {
+ float length = max(length(velocity), 0.001);
+ float logScale = log(length * 9.0 + 1.0) * warp;
+ return normalize(velocity) * logScale;
+ }
+
+ float fbm(vec2 p) {
+ // Note: Resolution is now controlled by canvas size
+
+ // Scale coordinates
+ p *= u_scale;
+
+ // Compute velocity field
+ vec2 velocity = getVelocityField(p, u_time);
+
+ // Apply warp control to intensify curl
+ velocity = applyWarpIntensity(velocity, u_warpStrength);
+
+ // Vary the advection time step based on phase shift
+ float dt = mix(0.2, 2.0, u_phaseShift);
+
+ // Advect along the curl field
+ float result = advect(p, velocity, u_time, dt);
+
+ // Return noise within proper range
+ return clamp(result, -1.0, 1.0);
+ }
+ `;
+
+ // Store shader sources for lazy loading
+ this.shaderSources = {
+ // gaussian: gaussianShader,
+ // perlin: perlinShader,
+ // cellular: cellularShader,
+ // fractal: fractalShader,
+ // waves: wavesShader,
+ domain_warp: domainWarpShader,
+ tensor_field: tensorFieldShader,
+ // heterogeneous_fbm: heterogeneousFbmShader,
+ // interference: interferenceShader,
+ // spectral: spectralShader,
+ // "3d_projection": projectionShader,
+ curl_noise: curlNoiseShader
+ };
+
+ // Initialize shader programs object
+ this.shaderPrograms = {};
+
+ // Store common shader parts
+ this.vertexShaderSource = vertexShaderSource;
+ this.fragmentShaderHeader = fragmentShaderHeader;
+ this.fragmentShaderFooter = fragmentShaderFooter;
+
+ // Track shader loading
+ this.pendingShaders = [];
+ this.loadingShader = false;
+
+ // Load only the default shader initially
+ const defaultShader = this.properties.shaderType || "domain_warp";
+ this.loadShader(defaultShader);
+
+ // Remove the background loading timer completely
+ // this.backgroundLoadTimer = setTimeout(() => {
+ // this.startBackgroundLoading();
+ // }, 2000); // Wait 2 seconds before starting background loads
+
+ // Create position buffer
+ const positionBuffer = this.gl.createBuffer();
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([
+ -1.0, -1.0,
+ 1.0, -1.0,
+ -1.0, 1.0,
+ 1.0, 1.0
+ ]), this.gl.STATIC_DRAW);
+
+ this.positionBuffer = positionBuffer;
+ };
+
+ // Helper function to create shader program
+ nodeType.prototype.createShaderProgram = function(vsSource, fsSource) {
+ const gl = this.gl;
+
+ // Create shaders
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
+ gl.shaderSource(vertexShader, vsSource);
+ gl.compileShader(vertexShader);
+
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
+ gl.shaderSource(fragmentShader, fsSource);
+ gl.compileShader(fragmentShader);
+
+ // Create program
+ const program = gl.createProgram();
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+ gl.linkProgram(program);
+
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ console.error('Shader program error:', gl.getProgramInfoLog(program));
+ return null;
+ }
+
+ return program;
+ };
+
+ // Load shader on demand
+ nodeType.prototype.loadShader = function(shaderType) {
+ // Skip shader loading if inactive
+ if (!this.isShaderActive && !this.properties.shaderVisible) {
+ return;
+ }
+
+ // Check if shader is already loaded
+ if (this.shaderPrograms[shaderType]) {
+ return;
+ }
+
+ // Check if shader source exists
+ if (!this.shaderSources[shaderType]) {
+ console.error('Shader source not found:', shaderType);
+ return;
+ }
+
+ // Add to pending shaders queue if not already there
+ if (!this.pendingShaders.includes(shaderType)) {
+ this.pendingShaders.push(shaderType);
+ }
+
+ // Start loading process if not already in progress
+ if (!this.loadingShader) {
+ this.processNextShader();
+ }
+ };
+
+ // Process the next shader in the queue
+ nodeType.prototype.processNextShader = function() {
+ // Skip processing if shader is now inactive
+ if (!this.isShaderActive && !this.properties.shaderVisible) {
+ this.loadingShader = false;
+ this.pendingShaders = []; // Clear pending shaders
+ return;
+ }
+
+ if (this.pendingShaders.length === 0) {
+ this.loadingShader = false;
+ return;
+ }
+
+ this.loadingShader = true;
+ const shaderType = this.pendingShaders.shift();
+
+ // Compile the shader program
+ console.log('Loading shader:', shaderType);
+ this.shaderPrograms[shaderType] = this.createShaderProgram(
+ this.vertexShaderSource,
+ this.fragmentShaderHeader + this.shaderSources[shaderType] + this.fragmentShaderFooter
+ );
+
+ // Process next shader on next frame to avoid blocking UI
+ setTimeout(() => {
+ this.processNextShader();
+ }, 10); // Short delay to let UI breathe
+ };
+
+ // Render the current shader
+ nodeType.prototype.renderShader = function() {
+ // Skip rendering if shader is inactive or WebGL context is not available
+ if (!this.isShaderActive || !this.gl || !this.shaderPrograms) return;
+
+ const gl = this.gl;
+ const currentShaderType = this.properties.shaderType;
+
+ // Check if the current shader program is loaded
+ if (!this.shaderPrograms[currentShaderType]) {
+ // Try to use a fallback shader while loading
+ const fallbackShader = Object.keys(this.shaderPrograms)[0];
+
+ // Request loading of the current shader
+ this.loadShader(currentShaderType);
+
+ // If no shaders are loaded at all, we can't render
+ if (!fallbackShader) {
+ // Draw loading indicator (using WebGL since 2D context may not be available)
+ gl.clearColor(0.1, 0.1, 0.1, 1.0); // Dark gray background
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ // We can't use text in WebGL easily, so we'll draw a simple loading indicator
+ // using a pulsing rectangle in the center
+ const now = performance.now() / 1000;
+ const pulseSize = 0.2 + 0.1 * Math.sin(now * 3); // Pulsing size
+
+ // Create a simple program for drawing a colored rectangle
+ if (!this.loadingIndicatorProgram) {
+ const vsSource = `
+ attribute vec2 a_position;
+ void main() {
+ gl_Position = vec4(a_position, 0.0, 1.0);
+ }
+ `;
+ const fsSource = `
+ precision mediump float;
+ uniform vec4 u_color;
+ void main() {
+ gl_FragColor = u_color;
+ }
+ `;
+ this.loadingIndicatorProgram = this.createShaderProgram(vsSource, fsSource);
+ }
+
+ if (this.loadingIndicatorProgram) {
+ gl.useProgram(this.loadingIndicatorProgram);
+
+ // Set color (light blue)
+ const colorLocation = gl.getUniformLocation(this.loadingIndicatorProgram, "u_color");
+ gl.uniform4f(colorLocation, 0.2, 0.6, 1.0, 1.0);
+
+ // Draw a rectangle in the center
+ const positionLocation = gl.getAttribLocation(this.loadingIndicatorProgram, "a_position");
+ gl.enableVertexAttribArray(positionLocation);
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
+
+ // Set viewport to maintain aspect ratio
+ gl.viewport(0, 0, this.shaderCanvas.width, this.shaderCanvas.height);
+
+ // Draw the rectangle
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+ }
+
+ return;
+ }
+
+ // Use fallback shader
+ const program = this.shaderPrograms[fallbackShader];
+ if (!program) return;
+
+ // Clear canvas
+ gl.clearColor(0, 0, 0, 1);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ // Update animation time
+ this.updateAnimationTime();
+
+ // Draw with fallback shader
+ this.drawShader(program);
+ return;
+ }
+
+ const program = this.shaderPrograms[currentShaderType];
+
+ // Clear canvas
+ gl.clearColor(0, 0, 0, 1);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ // Update animation time
+ this.updateAnimationTime();
+
+ // Draw with current shader
+ this.drawShader(program);
+ };
+
+ // Update animation time
+ nodeType.prototype.updateAnimationTime = function() {
+ // Only update if shader is active
+ if (!this.isShaderActive) {
+ return;
+ }
+
+ const now = performance.now();
+ if (this.properties.lastRenderTime > 0) {
+ const delta = (now - this.properties.lastRenderTime) / 1000;
+ this.properties.shaderTime += delta * this.properties.shaderSpeed;
+ }
+ this.properties.lastRenderTime = now;
+ };
+
+ // Draw using the provided shader program
+ nodeType.prototype.drawShader = function(program) {
+ const gl = this.gl;
+
+ // Use the provided shader program
+ gl.useProgram(program);
+
+ // Set common uniform values
+ const uniformLocations = {
+ time: gl.getUniformLocation(program, 'u_time'),
+ intensity: gl.getUniformLocation(program, 'u_intensity'),
+ scale: gl.getUniformLocation(program, 'u_scale'),
+ octaves: gl.getUniformLocation(program, 'u_octaves'),
+ persistence: gl.getUniformLocation(program, 'u_persistence'),
+ lacunarity: gl.getUniformLocation(program, 'u_lacunarity'),
+ shapeType: gl.getUniformLocation(program, 'u_shapeType'),
+ shapeStrength: gl.getUniformLocation(program, 'u_shapeStrength'),
+ warpStrength: gl.getUniformLocation(program, 'u_warpStrength'),
+ phaseShift: gl.getUniformLocation(program, 'u_phaseShift'),
+ frequencyRange: gl.getUniformLocation(program, 'u_frequencyRange'),
+ distribution: gl.getUniformLocation(program, 'u_distribution'),
+ adaptationStrength: gl.getUniformLocation(program, 'u_adaptationStrength'),
+ resolutionScale: gl.getUniformLocation(program, 'u_resolutionScale'),
+ colorScheme: gl.getUniformLocation(program, 'u_colorScheme')
+ };
+
+ // Set all uniforms
+ gl.uniform1f(uniformLocations.time, this.properties.shaderTime);
+ gl.uniform1f(uniformLocations.intensity, this.properties.shaderColorIntensity);
+ gl.uniform1f(uniformLocations.scale, this.properties.shaderScale);
+ gl.uniform1f(uniformLocations.octaves, this.properties.shaderOctaves);
+ gl.uniform1f(uniformLocations.persistence, 0.5); // Default value
+ gl.uniform1f(uniformLocations.lacunarity, 2.0); // Default value
+
+ // Map shapeType string to integer value
+ const shapeTypeMap = {
+ 'none': 0,
+ 'radial': 1,
+ 'linear': 2,
+ 'spiral': 3,
+ 'checkerboard': 4,
+ 'spots': 5,
+ 'hexgrid': 6,
+ 'stripes': 7,
+ 'gradient': 8,
+ 'vignette': 9,
+ 'cross': 10,
+ 'stars': 11,
+ 'triangles': 12,
+ 'concentric': 13,
+ 'rays': 14,
+ 'zigzag': 15
+ };
+ gl.uniform1i(uniformLocations.shapeType, shapeTypeMap[this.properties.shaderShapeType] || 0);
+
+ // Map colorScheme string to integer value
+ const colorSchemeMap = {
+ 'none': 0,
+ 'blue_red': 1,
+ 'viridis': 2,
+ 'plasma': 3,
+ 'inferno': 4,
+ 'magma': 5,
+ 'turbo': 6,
+ 'jet': 7,
+ 'rainbow': 8,
+ 'cool': 9,
+ 'hot': 10,
+ 'parula': 11,
+ 'hsv': 12,
+ 'autumn': 13,
+ 'winter': 14,
+ 'spring': 15,
+ 'summer': 16,
+ 'copper': 17,
+ 'pink': 18,
+ 'bone': 19,
+ 'ocean': 20,
+ 'terrain': 21,
+ 'neon': 22,
+ 'fire': 23
+ };
+ // Original line had an issue with " none\ (0) being treated as falsy: // gl.uniform1i(uniformLocations.colorScheme, colorSchemeMap[this.properties.colorScheme] || 1); // Default to blue_red
+
+ // Set colorScheme - properly handle "none" (value 0) without defaulting to blue_red
+ const colorSchemeValue = colorSchemeMap[this.properties.colorScheme];
+ gl.uniform1i(uniformLocations.colorScheme, colorSchemeValue !== undefined ? colorSchemeValue : 1); // Default to blue_red only if not found
+
+ gl.uniform1f(uniformLocations.shapeStrength, this.properties.shaderShapeStrength);
+ gl.uniform1f(uniformLocations.warpStrength, this.properties.shaderWarpStrength);
+ gl.uniform1f(uniformLocations.phaseShift, this.properties.shaderPhaseShift);
+ gl.uniform1i(uniformLocations.frequencyRange, this.properties.shaderFrequencyRange);
+ gl.uniform1i(uniformLocations.distribution, this.properties.shaderDistribution);
+ gl.uniform1f(uniformLocations.adaptationStrength, this.properties.shaderAdaptationStrength);
+
+ // We no longer need to send resolution to shader since we directly control pixel density via canvas size
+ // gl.uniform1f(uniformLocations.resolutionScale, this.properties.shaderResolutionScale);
+
+ // Set up position attribute
+ const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
+ gl.enableVertexAttribArray(positionAttributeLocation);
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
+ gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
+
+ // Draw
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+ };
+
+ // Override onDrawForeground to render the shader
+ const origOnDrawForeground = nodeType.prototype.onDrawForeground;
+ nodeType.prototype.onDrawForeground = function(ctx) {
+ // Render shader if visible
+ if (this.properties.shaderVisible && this.gl) {
+ // Calculate where to position the shader
+ let shaderWidth, shaderHeight, shaderY;
+
+ if (this.flags.collapsed) {
+ shaderWidth = 190;
+ shaderHeight = 20;
+ shaderY = 20; // Position below the title text
+ } else {
+ // Get the height of the node without shader area
+ const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
+ const baseHeight = baseSize[1];
+
+ // Set shader dimensions based on available space
+ shaderWidth = this.size[0];
+
+ // Use the current shaderHeight that's been set during resize
+ shaderHeight = this.shaderHeight;
+
+ // Ensure minimum height
+ if (!shaderHeight || shaderHeight < 50) {
+ shaderHeight = 50;
+ this.shaderHeight = 50;
+ }
+
+ shaderY = baseHeight;
+
+ // Resize the canvas if needed to match current dimensions
+ this.resizeShaderCanvas(shaderWidth, shaderHeight);
+ }
+
+ // Render shader to its canvas
+ this.renderShader();
+
+ // Call original onDrawForeground to draw the title
+ if (origOnDrawForeground) {
+ origOnDrawForeground.call(this, ctx);
+ }
+
+ // Save context for drawing with rounded corners
+ ctx.save();
+
+ // Define corner radius - not too large to maintain clean look
+ const cornerRadius = 8;
+
+ // Create clipping path with rounded corners for the shader display
+ if (!this.flags.collapsed) {
+ ctx.beginPath();
+ ctx.moveTo(0, shaderY + cornerRadius);
+ ctx.lineTo(0, shaderY + shaderHeight - cornerRadius);
+ ctx.arcTo(0, shaderY + shaderHeight, cornerRadius, shaderY + shaderHeight, cornerRadius);
+ ctx.lineTo(shaderWidth - cornerRadius, shaderY + shaderHeight);
+ ctx.arcTo(shaderWidth, shaderY + shaderHeight, shaderWidth, shaderY + shaderHeight - cornerRadius, cornerRadius);
+ ctx.lineTo(shaderWidth, shaderY + cornerRadius);
+ ctx.arcTo(shaderWidth, shaderY, shaderWidth - cornerRadius, shaderY, cornerRadius);
+ ctx.lineTo(cornerRadius, shaderY);
+ ctx.arcTo(0, shaderY, 0, shaderY + cornerRadius, cornerRadius);
+ ctx.closePath();
+ ctx.clip();
+ }
+
+ // Draw shader canvas
+ ctx.drawImage(
+ this.shaderCanvas,
+ 0, shaderY,
+ shaderWidth, shaderHeight
+ );
+
+ // Restore context after clipped drawing
+ ctx.restore();
+
+ // Draw a styled border with rounded corners around the shader
+ if (!this.flags.collapsed) {
+ // Save context for border drawing
+ ctx.save();
+
+ // Create path for border with rounded corners
+ ctx.beginPath();
+ ctx.moveTo(0, shaderY + cornerRadius);
+ ctx.lineTo(0, shaderY + shaderHeight - cornerRadius);
+ ctx.arcTo(0, shaderY + shaderHeight, cornerRadius, shaderY + shaderHeight, cornerRadius);
+ ctx.lineTo(shaderWidth - cornerRadius, shaderY + shaderHeight);
+ ctx.arcTo(shaderWidth, shaderY + shaderHeight, shaderWidth, shaderY + shaderHeight - cornerRadius, cornerRadius);
+ ctx.lineTo(shaderWidth, shaderY + cornerRadius);
+ ctx.arcTo(shaderWidth, shaderY, shaderWidth - cornerRadius, shaderY, cornerRadius);
+ ctx.lineTo(cornerRadius, shaderY);
+ ctx.arcTo(0, shaderY, 0, shaderY + cornerRadius, cornerRadius);
+ ctx.closePath();
+
+ // Create a subtle glow effect
+ ctx.shadowColor = "rgba(255, 255, 255, 0.2)";
+ ctx.shadowBlur = 2;
+ ctx.strokeStyle = "rgba(200, 200, 200, 0.6)";
+ ctx.lineWidth = 1;
+ ctx.stroke();
+
+ // Restore context
+ ctx.restore();
+ }
+
+ // Mark shader as active
+ this.isShaderActive = true;
+
+ // Request animation frame to keep updating - only when shader is active
+ // Store the requestAnimationFrame ID for potential cancellation
+ this.animationFrameId = requestAnimationFrame(() => {
+ // Only continue rendering if the shader is still active
+ if (this.isShaderActive && this.properties.shaderVisible) {
+ this.setDirtyCanvas(true, true);
+ }
+ });
+ } else {
+ // If shader not visible, just call original onDrawForeground
+ if (origOnDrawForeground) {
+ origOnDrawForeground.call(this, ctx);
+ }
+
+ // Ensure shader is marked as inactive
+ this.isShaderActive = false;
+
+ // Make sure no animation frame is pending
+ if (this.animationFrameId) {
+ cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = null;
+ }
+ }
+ };
+
+ // Override computeSize to adjust node height
+ const origComputeSize = nodeType.prototype.computeSize;
+ nodeType.prototype.computeSize = function(size) {
+ size = size || [this.size[0] || 300, 0];
+
+ // Handle collapsed state
+ if (this.flags && this.flags.collapsed) {
+ size[0] = 190; // Collapsed width
+ size[1] = 40; // Collapsed height (title + shader)
+ return size;
+ }
+
+ // Get base size without shader
+ if (origComputeSize) {
+ origComputeSize.call(this, size);
+ }
+
+ // Store the base height before adding shader height
+ const baseHeight = size[1];
+
+ // Add height for shader area if visible
+ if (this.properties && this.properties.shaderVisible) {
+ // If this is called during user resize, respect the provided size
+ // This is crucial for allowing Y-axis shrinking
+ if (this._isResizing) {
+ // The actual shader height will be calculated in onResize
+ return size;
+ }
+
+ // Otherwise use the stored shader height for normal rendering
+ const minShaderHeight = 50;
+ const currentShaderHeight = this.shaderHeight || 200;
+
+ // Set the size to be exactly baseHeight + shaderHeight
+ size[1] = baseHeight + Math.max(minShaderHeight, currentShaderHeight);
+ }
+
+ return size;
+ };
+
+ // Add clean-up on node removal
+ const origOnRemoved = nodeType.prototype.onRemoved;
+ nodeType.prototype.onRemoved = function() {
+ if (origOnRemoved) {
+ origOnRemoved.call(this);
+ }
+
+ // Cancel any pending animation frame
+ if (this.animationFrameId) {
+ cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = null;
+ }
+
+ // Clean up WebGL resources
+ if (this.gl) {
+ // Delete shader programs
+ for (const programType in this.shaderPrograms) {
+ if (this.shaderPrograms[programType]) {
+ this.gl.deleteProgram(this.shaderPrograms[programType]);
+ }
+ }
+
+ // Delete loading indicator program if it exists
+ if (this.loadingIndicatorProgram) {
+ this.gl.deleteProgram(this.loadingIndicatorProgram);
+ this.loadingIndicatorProgram = null;
+ }
+
+ // Delete buffers
+ if (this.positionBuffer) {
+ this.gl.deleteBuffer(this.positionBuffer);
+ }
+
+ // Clear references
+ this.shaderPrograms = null;
+ this.positionBuffer = null;
+ this.gl = null;
+ }
+
+ // Clear shader sources to save memory
+ this.shaderSources = null;
+
+ // Remove canvas reference
+ this.shaderCanvas = null;
+
+ // Clear any pending shaders
+ this.pendingShaders = [];
+ this.loadingShader = false;
+ };
+
+ // Start loading remaining shaders in the background with longer delays between each
+ nodeType.prototype.startBackgroundLoading = function() {
+ // This function is now a no-op - we only want to load shaders on demand
+ return;
+ };
+ }
+ }
+});