feature modulation of particle systems

This commit is contained in:
ryanontheinstide
2024-08-29 12:24:22 -04:00
parent d79ac2936c
commit 98b21aaa50
14 changed files with 73 additions and 10646 deletions
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 KiB

+5
View File
@@ -288,6 +288,7 @@ add_node_config("ParticleModulationBase", {
- `temporal_easing`: Easing function for the modulation effect ("ease_in_out", "linear", "bounce", "elastic", "none")
- `palindrome`: Whether to reverse the modulation effect after completion (True/False)
- `random`: Selects a random value between 0 and the chosen target value and applies it per particle
- `feature`: Optionally, pass in a feature (like audio or motion) to drive the modulation of the particles
"""
})
@@ -543,6 +544,10 @@ Direction Control:
- `direction_frequency`: How quickly the emission angle changes (0.0 to 10.0). Higher values create more rapid direction changes.
- `direction_amplitude`: Maximum angle change in degrees (0.0 to 180.0). Larger values allow for more extreme direction shifts.
Feature Modulation:
- `feature`: Optional feature to modulate the movement (FEATURE type)
- `feature_param`: Parameter to be modulated by the feature ("emitter_x_frequency", "emitter_y_frequency", or "direction_frequency")
"""
})
+34 -14
View File
@@ -9,6 +9,14 @@ class FeatureModulationBase(RyanOnTheInside):
CATEGORY = "RyanOnTheInside/FlexFeatures"
FUNCTION = "modulate"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"invert_output": ("BOOLEAN", {"default": False}),
}
}
def visualize(self, feature, width=1920, height=1080):
values = [feature.get_value_at_frame(i) for i in range(feature.frame_count)]
frames = len(values)
@@ -58,14 +66,21 @@ class FeatureModulationBase(RyanOnTheInside):
return img_tensor
def create_processed_feature(self, original_feature, processed_values, name_prefix="Processed"):
def create_processed_feature(self, original_feature, processed_values, name_prefix="Processed", invert_output=False):
class ProcessedFeature(type(original_feature)):
def __init__(self, original_feature, processed_values):
def __init__(self, original_feature, processed_values, invert_output):
self.__dict__.update(original_feature.__dict__)
self.name = f"{name_prefix}_{original_feature.name}"
if invert_output:
self.name = f"Inverted_{self.name}"
self.frame_rate = original_feature.frame_rate
self.frame_count = len(processed_values)
self.data = processed_values
if invert_output:
min_val, max_val = min(processed_values), max(processed_values)
self.data = [max_val - v + min_val for v in processed_values]
else:
self.data = processed_values
def extract(self):
return self
@@ -73,7 +88,7 @@ class FeatureModulationBase(RyanOnTheInside):
def get_value_at_frame(self, frame_index):
return self.data[frame_index]
return ProcessedFeature(original_feature, processed_values)
return ProcessedFeature(original_feature, processed_values, invert_output)
class FeatureMixer(FeatureModulationBase):
@classmethod
@@ -92,6 +107,7 @@ class FeatureMixer(FeatureModulationBase):
"feature_threshold": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"rise_detection_threshold": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.05}),
"rise_smoothing_factor": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 2.0, "step": 0.05}),
**super().INPUT_TYPES()["required"], # Include the invert_output option
}
}
@@ -99,7 +115,7 @@ class FeatureMixer(FeatureModulationBase):
RETURN_NAMES = ("FEATURE", "FEATURE_VISUALIZATION")
FUNCTION = "modulate"
def modulate(self, feature, base_gain, floor, ceiling, peak_sharpness, valley_sharpness, attack, release, smoothing, feature_threshold, rise_detection_threshold, rise_smoothing_factor):
def modulate(self, feature, base_gain, floor, ceiling, peak_sharpness, valley_sharpness, attack, release, smoothing, feature_threshold, rise_detection_threshold, rise_smoothing_factor, invert_output):
values = [feature.get_value_at_frame(i) for i in range(feature.frame_count)]
values = [v if v >= feature_threshold else 0 for v in values]
@@ -144,7 +160,7 @@ class FeatureMixer(FeatureModulationBase):
# chop
final_values = [max(floor, min(ceiling, v)) for v in adjusted]
processed_feature = self.create_processed_feature(feature, final_values, "Processed")
processed_feature = self.create_processed_feature(feature, final_values, "Processed", invert_output)
return (processed_feature, self.visualize(processed_feature))
def apply_rise_time_adjustment(self, values, rise_detection_threshold, rise_smoothing_factor):
@@ -180,13 +196,14 @@ class FeatureScaler(FeatureModulationBase):
"min_output": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"max_output": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"exponent": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 10.0, "step": 0.1}),
**super().INPUT_TYPES()["required"],
}
}
RETURN_TYPES = ("FEATURE", "IMAGE")
RETURN_NAMES = ("FEATURE", "FEATURE_VISUALIZATION")
def modulate(self, feature, scale_type, min_output, max_output, exponent):
def modulate(self, feature, scale_type, min_output, max_output, exponent, invert_output):
values = [feature.get_value_at_frame(i) for i in range(feature.frame_count)]
min_val, max_val = min(values), max(values)
@@ -203,7 +220,7 @@ class FeatureScaler(FeatureModulationBase):
final_values = [min_output + v * (max_output - min_output) for v in scaled]
processed_feature = self.create_processed_feature(feature, final_values, "Scaled")
processed_feature = self.create_processed_feature(feature, final_values, "Scaled", invert_output)
return (processed_feature, self.visualize(processed_feature))
class FeatureMath(FeatureModulationBase):
@@ -216,13 +233,14 @@ class FeatureMath(FeatureModulationBase):
"operation": (["add", "subtract", "multiply", "divide", "max", "min"],),
"weight1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"weight2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
**super().INPUT_TYPES()["required"],
}
}
RETURN_TYPES = ("FEATURE", "IMAGE")
RETURN_NAMES = ("FEATURE", "FEATURE_VISUALIZATION")
def modulate(self, feature1, feature2, operation, weight1, weight2):
def modulate(self, feature1, feature2, operation, weight1, weight2, invert_output):
values1 = [feature1.get_value_at_frame(i) for i in range(feature1.frame_count)]
values2 = [feature2.get_value_at_frame(i) for i in range(feature2.frame_count)]
@@ -244,7 +262,7 @@ class FeatureMath(FeatureModulationBase):
elif operation == "min":
combined = [min(weight1 * v1, weight2 * v2) for v1, v2 in zip(values1, values2)]
processed_feature = self.create_processed_feature(feature1, combined, "Combined")
processed_feature = self.create_processed_feature(feature1, combined, "Combined", invert_output)
return (processed_feature, self.visualize(processed_feature))
class FeatureSmoothing(FeatureModulationBase):
@@ -257,13 +275,14 @@ class FeatureSmoothing(FeatureModulationBase):
"window_size": ("INT", {"default": 5, "min": 3, "max": 21, "step": 2}),
"alpha": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.01}),
"sigma": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 5.0, "step": 0.1}),
**super().INPUT_TYPES()["required"],
}
}
RETURN_TYPES = ("FEATURE", "IMAGE")
RETURN_NAMES = ("FEATURE", "FEATURE_VISUALIZATION")
def modulate(self, feature, smoothing_type, window_size, alpha, sigma):
def modulate(self, feature, smoothing_type, window_size, alpha, sigma, invert_output):
values = [feature.get_value_at_frame(i) for i in range(feature.frame_count)]
if smoothing_type == "moving_average":
@@ -281,7 +300,7 @@ class FeatureSmoothing(FeatureModulationBase):
kernel = kernel / np.sum(kernel)
smoothed = np.convolve(values, kernel, mode='same')
processed_feature = self.create_processed_feature(feature, smoothed, "Smoothed")
processed_feature = self.create_processed_feature(feature, smoothed, "Smoothed", invert_output)
return (processed_feature, self.visualize(processed_feature))
class FeatureOscillator(FeatureModulationBase):
@@ -295,13 +314,14 @@ class FeatureOscillator(FeatureModulationBase):
"amplitude": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"phase_shift": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2*np.pi, "step": 0.1}),
"blend": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
**super().INPUT_TYPES()["required"],
}
}
RETURN_TYPES = ("FEATURE", "IMAGE")
RETURN_NAMES = ("FEATURE", "FEATURE_VISUALIZATION")
def modulate(self, feature, oscillator_type, frequency, amplitude, phase_shift, blend):
def modulate(self, feature, oscillator_type, frequency, amplitude, phase_shift, blend, invert_output):
values = [feature.get_value_at_frame(i) for i in range(feature.frame_count)]
t = np.linspace(0, 2*np.pi, len(values))
@@ -316,5 +336,5 @@ class FeatureOscillator(FeatureModulationBase):
blended = [v * (1 - blend) + osc * blend for v, osc in zip(values, oscillation)]
processed_feature = self.create_processed_feature(feature, blended, "Oscillated")
processed_feature = self.create_processed_feature(feature, blended, "Oscillated", invert_output)
return (processed_feature, self.visualize(processed_feature))
+4 -4
View File
@@ -141,8 +141,8 @@ class FlexMaskVoronoiScheduled(FlexMaskBase):
"y_offset": ("FLOAT", {"default": 0.0, "min": -1000.0, "max": 1000.0, "step": 0.1}),
"feature_param": (["scale", "detail", "randomness", "seed", "x_offset", "y_offset"],),
"formula": (list(cls.formulas.keys()),),
"a": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
"b": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
"x_offset": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"y_offset": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}),
}
}
@@ -170,9 +170,9 @@ class FlexMaskVoronoiScheduled(FlexMaskBase):
elif feature_param == "seed":
seed = int(seed + (schedule_value * strength * 1000000))
elif feature_param == "x_offset":
x_offset *= (1 + schedule_value * strength)
x_offset += width * schedule_value * strength
elif feature_param == "y_offset":
y_offset *= (1 + schedule_value * strength)
y_offset += height * schedule_value * strength
# Create VoronoiNoise instance
voronoi = VoronoiNoise(
+14 -1
View File
@@ -556,7 +556,8 @@ class ParticleSystemMaskBase(MaskBase, ABC):
'effect_duration': effect_duration,
'temporal_easing': modulation['temporal_easing'],
'palindrome': modulation['palindrome'],
'random': modulation.get('random', False) # Add this line
'random': modulation.get('random', False),
'feature': modulation.get('feature',None)
}
if modulation['type'] == 'ParticleSizeModulation':
@@ -580,6 +581,8 @@ class ParticleSystemMaskBase(MaskBase, ABC):
if modulation['start_frame'] <= current_frame < modulation['end_frame']:
if modulation.get('random', False):
progress = random.random() # Generate a random value between 0 and 1
elif 'feature' in modulation and modulation['feature'] is not None:
progress = modulation['feature'].get_value_at_frame(current_frame)
else:
progress = self.calculate_modulation_progress(current_frame, modulation, particle.creation_frame)
@@ -791,6 +794,16 @@ class ParticleSystemMaskBase(MaskBase, ABC):
if 'movement' in emitter:
movement = emitter['movement']
# Check if a feature is present and apply it to the specified parameter
if 'feature' in movement and movement['feature'] is not None:
feature = movement['feature']
feature_param = movement['feature_param']
feature_value = feature.get_value_at_frame(frame_index)
if feature_param in ['emitter_x_frequency', 'emitter_y_frequency', 'direction_frequency']:
# Use 1 + feature_value to ensure the frequency is always at least the original value
movement[feature_param] *= (1 + feature_value)
# Modulate emitter_x
base_x = float(emitter['initial_x'])
freq_x = float(movement['emitter_x_frequency'])
+11 -3
View File
@@ -238,6 +238,10 @@ class EmitterMovement(ParticleSystemModulatorBase):
"emitter_y_amplitude": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 0.5, "step": 0.01}),
"direction_frequency": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"direction_amplitude": ("FLOAT", {"default": 180.0, "min": 0.0, "max": 360.0, "step": 1.0}),
"feature_param": (["emitter_x_frequency", "emitter_y_frequency", "direction_frequency"],),
},
"optional": {
"feature": ("FEATURE",),
}
}
@@ -245,7 +249,8 @@ class EmitterMovement(ParticleSystemModulatorBase):
FUNCTION = "create_movement"
def create_movement(self, emitter_x_frequency, emitter_y_frequency, direction_frequency,
emitter_x_amplitude, emitter_y_amplitude, direction_amplitude):
emitter_x_amplitude, emitter_y_amplitude, direction_amplitude,
feature_param, feature=None):
movement = {
"emitter_x_frequency": emitter_x_frequency,
"emitter_y_frequency": emitter_y_frequency,
@@ -253,6 +258,8 @@ class EmitterMovement(ParticleSystemModulatorBase):
"emitter_x_amplitude": emitter_x_amplitude,
"emitter_y_amplitude": emitter_y_amplitude,
"direction_amplitude": direction_amplitude,
"feature_param": feature_param,
"feature": feature,
}
return (movement,)
@@ -315,11 +322,11 @@ class ParticleModulationBase(ParticleSystemModulatorBase):
},
"optional": {
"previous_modulation": ("PARTICLE_MODULATION",),
"feature": ("FEATURE",),
}
}
def create_modulation(self, start_frame, end_frame, effect_duration, temporal_easing, palindrome, random, previous_modulation=None):
def create_modulation(self, start_frame, end_frame, effect_duration, temporal_easing, palindrome, random, previous_modulation=None, feature=None):
modulation = {
"start_frame": start_frame,
"end_frame": end_frame,
@@ -327,6 +334,7 @@ class ParticleModulationBase(ParticleSystemModulatorBase):
"temporal_easing": temporal_easing,
"palindrome": palindrome,
"random": random,
"feature": feature,
}
modulation_type = self.__class__.__name__
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_ryanonyheinside"
description = "Custom nodes introducing particle simulations, optical flow, audio manipulation & reactivity, and temporal masks"
version = "1.4.3"
version = "1.4.6"
license = {file = "LICENSE"}
dependencies = ["opencv-python==4.10.0","scipy","pillow","librosa","pymunk==6.8.1","matplotlib","openunmix","mido"]
+4 -4
View File
@@ -36,18 +36,18 @@ Getting started with the RyanOnTheInside node pack is easy:
2. Open ComfyUI and look for nodes prefixed with "RyanOnTheInside" in the node browser.
3. Check out the example workflows on Civitai to see how different features can be used.
### Example Workflows on Civitai
### Example and Tutorial Workflows on Civitai
For a range of example workflows showcasing the capabilities of this node pack, visit my Civitai profile:
For a range of example and tutorial workflows showcasing the capabilities of this node pack, visit my Civitai profile:
[RyanOnTheInside Civitai Profile](https://civitai.com/user/RyanOnTheInside)
## 🚀 Key Features
Particles are now reactive!
### 🎛️ Flex Features
Dynamic control over various aspects of your workflow:
- Modulate IPAdapters, Masks, and Images based on extracted features
- Modulate IPAdapters, Masks, Images, and Particles based on extracted features
- Features include: Audio, MIDI, Motion, Depth, Color, Time, and more
- Create adaptive, responsive effects that evolve with your input data