feature modulation of particle systems
This commit is contained in:
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 |
@@ -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")
|
||||
|
||||
|
||||
"""
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user