Merge branch 'master'

This commit is contained in:
Æmotion
2025-05-22 14:52:32 -07:00
18 changed files with 19180 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
# ComfyUI-ShaderNoiseKSampler
![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)
![ComfyUI](https://img.shields.io/badge/ComfyUI-compatible-green)
![License](https://img.shields.io/badge/license-GPL--3.0-brightgreen.svg)
![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen.svg)
**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.
![ShaderNoiseKSampler Showcase WEBP](images/shader_noise_sampler_action.webp)
*(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
![ShaderNoiseKSampler Node PNG](images/shader_noise_sampler_node.png)
*(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:
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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).
+76
View File
@@ -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"]
+11
View File
@@ -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
}
+299
View File
@@ -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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+609
View File
@@ -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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+562
View File
@@ -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
File diff suppressed because it is too large Load Diff
+245
View File
@@ -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();
}
+13
View File
@@ -0,0 +1,13 @@
<svg width="241" height="194" viewBox="0 0 241 194" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1_219" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-1" y="0" width="242" height="194">
<path d="M240.469 0.958984H-0.00585938V193.918H240.469V0.958984Z" fill="white"/>
</mask>
<g mask="url(#mask0_1_219)">
<path d="M96.1344 193.911C61.1312 193.911 32.6597 178.256 15.9721 149.829C1.19788 124.912 -0.00585938 97.9229 -0.00585938 67.7662C-0.00585938 49.8876 5.37293 34.3215 15.5413 22.7466C24.8861 12.1157 38.1271 5.22907 52.8317 3.35378C70.2858 1.14271 91.9848 0.958984 114.545 0.958984C151.259 0.958984 161.63 1.4088 176.075 2.85328C195.29 4.76026 211.458 11.932 222.824 23.5955C234.368 35.4428 240.469 51.2624 240.469 69.3627V72.9994C240.469 103.885 219.821 129.733 191.046 136.759C188.898 141.827 186.237 146.871 183.089 151.837L183.006 151.964C172.869 167.632 149.042 193.918 103.401 193.918H96.1281L96.1344 193.911Z" fill="white"/>
<path d="M174.568 17.9772C160.927 16.6151 151.38 16.1589 114.552 16.1589C90.908 16.1589 70.9008 16.387 54.7644 18.4334C33.3949 21.164 15.2058 37.5285 15.2058 67.7674C15.2058 98.0066 16.796 121.422 29.0741 142.107C42.9425 165.751 66.1302 178.707 96.1412 178.707H103.414C140.242 178.707 160.25 159.156 170.253 143.698C174.574 136.874 177.754 130.058 179.801 123.234C205.947 120.96 225.27 99.3624 225.27 72.9941V69.3577C225.27 40.9432 206.631 21.164 174.574 17.9772H174.568Z" fill="white"/>
<path d="M15.1975 67.7674C15.1975 37.5285 33.3866 21.164 54.7559 18.4334C70.8987 16.387 90.906 16.1589 114.544 16.1589C151.372 16.1589 160.919 16.6151 174.559 17.9772C206.617 21.1576 225.255 40.937 225.255 69.3577V72.9941C225.255 99.3687 205.932 120.966 179.786 123.234C177.74 130.058 174.559 136.874 170.238 143.698C160.235 159.156 140.228 178.707 103.4 178.707H96.1264C66.1155 178.707 42.9277 165.751 29.0595 142.107C16.7814 121.422 15.1912 98.4563 15.1912 67.7674" fill="#202020"/>
<path d="M32.2469 67.9899C32.2469 97.3168 34.0654 116.184 43.6127 133.689C54.5225 153.924 74.3018 161.653 96.8117 161.653H103.857C133.411 161.653 147.736 147.329 155.693 134.829C159.558 128.462 162.966 121.417 164.784 112.547L166.147 106.864H174.332C192.521 106.864 208.208 92.09 208.208 73.2166V69.8082C208.208 48.6669 195.024 37.5228 172.058 34.7987C159.102 33.6646 151.372 33.2084 114.538 33.2084C89.7602 33.2084 72.0272 33.4364 58.6152 35.4828C39.7483 38.2134 32.2407 48.8951 32.2407 67.9899" fill="white"/>
<path d="M166.158 83.6801C166.158 86.4107 168.204 88.4572 171.841 88.4572C183.435 88.4572 189.802 81.8619 189.802 70.9523C189.802 60.0427 183.435 53.2195 171.841 53.2195C168.204 53.2195 166.158 55.2657 166.158 57.9963V83.6866V83.6801Z" fill="#202020"/>
<path d="M54.5321 82.3198C54.5321 95.732 62.0332 107.326 71.5807 116.424C77.9478 122.562 87.9515 128.93 94.7685 133.022C96.8147 134.157 98.8611 134.841 101.136 134.841C103.866 134.841 106.134 134.157 107.959 133.022C114.782 128.93 124.779 122.562 130.919 116.424C140.694 107.332 148.195 95.7383 148.195 82.3198C148.195 67.7673 137.286 54.8115 121.599 54.8115C112.28 54.8115 105.912 59.5882 101.136 66.1772C96.8147 59.582 90.2259 54.8115 80.9001 54.8115C64.9855 54.8115 54.5256 67.7673 54.5256 82.3198" fill="#FF5A16"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+3095
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+420
View File
@@ -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;
};
}
}
});
File diff suppressed because it is too large Load Diff