implemented basic mesh animation

This commit is contained in:
paulh4x
2025-07-15 01:01:09 +02:00
parent b786956fb4
commit 1ff340d810
5 changed files with 221 additions and 33 deletions
+8 -1
View File
@@ -214,6 +214,7 @@ This wrapper provides a comprehensive set of nodes to build 3D scenes.
#### Animation
- **RenderFormer Camera Target**: Creates a camera animation sequence by defining a start and end camera state (position, look-at, FOV).
- **RenderFormer Lighting Target**: Creates a light animation sequence by defining a start and end light state (position, rotation, scale, and emission).
- **RenderFormer Mesh Target**: Creates a mesh animation sequence by defining a start and end state for a mesh's transformation (position, rotation, scale) and material properties (diffuse color, specular color, roughness).
#### Utilities
- **RenderFormer Mesh Combine**: Combines multiple `MESH` outputs into a single object list.
@@ -254,7 +255,7 @@ This project is under active development. Here is a summary of the progress so f
- [ ] **Animation Flickering:** Investigate and fix flickering in animations, especially for objects with highly reflective materials.
- [ ] **Mesh Format Support:** Add support for loading `.glb` and `.fbx` files, including their materials and textures.
- [ ] **Mesh Animation:** Implement animation capabilities for `MESH` properties (translation, rotation, scale).
- [x] **Mesh Animation:** Implemented animation capabilities for `MESH` properties (translation, rotation, scale) and materials (diffuse, specular, roughness).
- [ ] **Camera Adoption:** Integrate with the `Load 3D` core node to adopt its camera transformations.
- [ ] **Material Presets:** Create a system for saving and loading material presets.
- [ ] **Public Release:** Prepare for a more stable, public release with better documentation and examples.
@@ -263,6 +264,12 @@ This project is under active development. Here is a summary of the progress so f
### 📜 Version History
#### Version 0.4.0 - Mesh Animation
- **Feature: Mesh & Material Animation:** A complete animation workflow for meshes has been implemented.
- **New Node: `RenderFormerMeshTarget`:** This new node allows you to define an animation for a mesh by specifying its end-state transformation (position, rotation, scale) and material properties (diffuse color, specular color, roughness).
- **Enhancement: `RenderFormerMeshCombine`:** This node has been upgraded to intelligently handle both static meshes and the new animated mesh sequences, allowing you to combine multiple animated and static objects in a scene.
- **Enhancement: `RenderFormerSceneBuilder`:** The scene builder now correctly processes the new mesh animation sequence, enabling it to generate interpolated frames for mesh animations just as it does for cameras and lights.
#### Version 0.3.35 - Bug Fixes & Stability
- **Fix:** Resolved a critical `WrongTypeError` in the `RenderFormerRandomizeColors` node. The `random_diffuse_type` was being set to an incorrect string value (`"per triangle"`), which has now been corrected to the valid literal (`"per-triangle"`).
- **Fix:** The `per-shading` mode in the `RenderFormerRandomizeColors` node has been updated to use the correct literal `"per-shading-group"` for consistency with the scene configuration, ensuring it functions as expected.
+1 -1
View File
@@ -16,7 +16,7 @@ logo = fr"""
{dark_yellow} __/ __ | / -_) \ _` | -_) _| _| _ \ _| ` \ -_) _|{reset}
{dark_yellow} _| _| _| _|_\ \___| _| _| \__,_| \___| _| _| \___/ _| _|_|_| \___| _|{reset}
{dark_grey} -->> PH's RenderFormer Wrapper for ComfyUI Version: 0.3.35 <<--{reset}
{dark_grey} -->> PH's RenderFormer Wrapper for ComfyUI Version: 0.4.0 <<--{reset}
"""
print(logo)
File diff suppressed because one or more lines are too long
+210 -30
View File
@@ -571,6 +571,7 @@ class RenderFormerSceneBuilder:
"camera_sequence": ("CAMERA_SEQUENCE",),
"camera_sequence": ("CAMERA_SEQUENCE",),
"lighting_sequence": ("LIGHTING",),
"mesh_sequence": ("MESH_SEQUENCE",),
"num_frames": ("INT", {"default": 1, "min": 1, "max": 1000}),
"add_default_background": ("BOOLEAN", {"default": False}),
}
@@ -580,11 +581,11 @@ class RenderFormerSceneBuilder:
FUNCTION = "build_scene"
CATEGORY = "PHRenderFormer"
def build_scene(self, mesh, lighting, camera, camera_sequence=None, lighting_sequence=None, num_frames=1, add_default_background=False):
def build_scene(self, mesh, lighting, camera, camera_sequence=None, lighting_sequence=None, mesh_sequence=None, num_frames=1, add_default_background=False):
output_scene = None
output_sequence = None
is_animation = num_frames > 1 and (camera_sequence is not None or lighting_sequence is not None)
is_animation = num_frames > 1 and (camera_sequence is not None or lighting_sequence is not None or mesh_sequence is not None)
if not is_animation:
# Build a single static scene
@@ -598,13 +599,55 @@ class RenderFormerSceneBuilder:
# --- Determine Start and End Configurations ---
# The 'camera' and 'lighting' inputs are ALWAYS the start frame.
start_cam_config = camera
start_lighting_config = lighting
# For animations, the sequence inputs are the source of truth for BOTH start and end frames.
# If a sequence isn't provided, we fall back to the static input for that element.
start_cam_config = camera_sequence["sequence"][0] if camera_sequence and "sequence" in camera_sequence and camera_sequence["sequence"] else camera
end_cam_config = camera_sequence["sequence"][-1] if camera_sequence and "sequence" in camera_sequence and camera_sequence["sequence"] else camera
start_lighting_config = lighting_sequence["start_lights"] if lighting_sequence and isinstance(lighting_sequence, dict) and "start_lights" in lighting_sequence else lighting
end_lighting_config = lighting_sequence["end_lights"] if lighting_sequence and isinstance(lighting_sequence, dict) and "end_lights" in lighting_sequence else lighting
# --- Mesh Configuration: Smartly combine static and animated meshes ---
if not (mesh_sequence and isinstance(mesh_sequence, dict)):
# If there's no animation sequence, we just use the static meshes.
start_mesh_config = mesh
end_mesh_config = copy.deepcopy(mesh) if mesh else None
else:
# An animation sequence exists. We need to filter out the animated mesh's
# static counterpart from the main 'mesh' input to avoid duplication.
static_meshes = mesh.get("meshes", []) if mesh else []
static_materials = mesh.get("materials", []) if mesh else []
static_transforms = mesh.get("transforms", []) if mesh else []
animated_start_data = mesh_sequence.get("start_meshes", {})
animated_start_meshes = animated_start_data.get("meshes", [])
# These lists will hold the static meshes that are NOT being animated.
clean_static_meshes, clean_static_materials, clean_static_transforms = [], [], []
# Iterate through the static meshes and keep only the ones that are not
# also present in the animated list (by object identity).
for i, static_mesh_obj in enumerate(static_meshes):
if not any(static_mesh_obj is anim_mesh_obj for anim_mesh_obj in animated_start_meshes):
clean_static_meshes.append(static_mesh_obj)
clean_static_materials.append(static_materials[i])
clean_static_transforms.append(static_transforms[i])
# The start frame is the clean static list plus the start state of the animated meshes.
start_mesh_config = {
"meshes": clean_static_meshes + animated_start_data.get("meshes", []),
"materials": clean_static_materials + animated_start_data.get("materials", []),
"transforms": clean_static_transforms + animated_start_data.get("transforms", [])
}
# The end frame is a copy of the clean static list plus the end state of the animated meshes.
animated_end_data = mesh_sequence.get("end_meshes", {})
end_mesh_config = {
"meshes": copy.deepcopy(clean_static_meshes) + animated_end_data.get("meshes", []),
"materials": copy.deepcopy(clean_static_materials) + animated_end_data.get("materials", []),
"transforms": copy.deepcopy(clean_static_transforms) + animated_end_data.get("transforms", [])
}
# The '..._sequence' inputs provide the end frame. If not provided, the end state is the same as the start.
end_cam_config = camera_sequence["sequence"][-1] if camera_sequence and "sequence" in camera_sequence and camera_sequence["sequence"] else start_cam_config
# Check if lighting_sequence is a dictionary (i.e., an animation) before accessing its keys.
end_lighting_config = lighting_sequence["end_lights"] if lighting_sequence and isinstance(lighting_sequence, dict) and "end_lights" in lighting_sequence else start_lighting_config
# --- Build Keyframe Scenes in completely isolated directories ---
start_scene = None
@@ -613,14 +656,14 @@ class RenderFormerSceneBuilder:
with tempfile.TemporaryDirectory() as start_tmpdir, tempfile.TemporaryDirectory() as end_tmpdir:
# Build scene for the START frame
print("PHRenderFormer: Building start frame...")
start_scene = self._build_single_scene(start_tmpdir, mesh, start_cam_config, start_lighting_config, add_default_background)
start_scene = self._build_single_scene(start_tmpdir, start_mesh_config, start_cam_config, start_lighting_config, add_default_background)
if not start_scene:
raise Exception("Failed to build the start frame for interpolation.")
pbar.update(1)
# Build scene for the END frame
print("PHRenderFormer: Building end frame...")
end_scene = self._build_single_scene(end_tmpdir, mesh, end_cam_config, end_lighting_config, add_default_background)
end_scene = self._build_single_scene(end_tmpdir, end_mesh_config, end_cam_config, end_lighting_config, add_default_background)
if not end_scene:
raise Exception("Failed to build the end frame for interpolation.")
pbar.update(1)
@@ -810,31 +853,166 @@ class RenderFormerMeshCombine:
}
}
RETURN_TYPES = ("MESH",)
RETURN_TYPES = ("MESH", "MESH_SEQUENCE",)
RETURN_NAMES = ("MESH", "MESH_SEQUENCE",)
FUNCTION = "combine_meshes"
CATEGORY = "PHRenderFormer"
def combine_meshes(self, mesh_1, **kwargs):
all_meshes = []
all_materials = []
all_transforms = []
def combine_meshes(self, **kwargs):
# Lists to hold the components for the start and end frames
all_start_meshes, all_start_materials, all_start_transforms = [], [], []
all_end_meshes, all_end_materials, all_end_transforms = [], [], []
# Add the first mesh and material
all_meshes.extend(mesh_1["meshes"])
all_materials.extend(mesh_1["materials"])
all_transforms.extend(mesh_1["transforms"])
# Add subsequent meshes and materials from optional inputs
for key in sorted(kwargs.keys()):
mesh = kwargs.get(key)
if mesh and isinstance(mesh, dict) and "meshes" in mesh and "materials" in mesh and "transforms" in mesh:
all_meshes.extend(mesh["meshes"])
all_materials.extend(mesh["materials"])
all_transforms.extend(mesh["transforms"])
combined_ph_mesh = {"meshes": all_meshes, "materials": all_materials, "transforms": all_transforms}
# Iterate through all possible mesh inputs
for i in range(1, 9):
mesh_input = kwargs.get(f"mesh_{i}")
if not mesh_input:
continue
return (combined_ph_mesh,)
# Case 1: Input is an animated MESH_SEQUENCE
if isinstance(mesh_input, dict) and "start_meshes" in mesh_input and "end_meshes" in mesh_input:
start_data = mesh_input["start_meshes"]
end_data = mesh_input["end_meshes"]
all_start_meshes.extend(start_data["meshes"])
all_start_materials.extend(start_data["materials"])
all_start_transforms.extend(start_data["transforms"])
all_end_meshes.extend(end_data["meshes"])
all_end_materials.extend(end_data["materials"])
all_end_transforms.extend(end_data["transforms"])
# Case 2: Input is a static MESH object
elif isinstance(mesh_input, dict) and "meshes" in mesh_input:
# For a static mesh, its properties are the same for both start and end frames.
# We do a deepcopy for the end frame to prevent any potential downstream mutation issues.
all_start_meshes.extend(mesh_input["meshes"])
all_start_materials.extend(mesh_input["materials"])
all_start_transforms.extend(mesh_input["transforms"])
all_end_meshes.extend(copy.deepcopy(mesh_input["meshes"]))
all_end_materials.extend(copy.deepcopy(mesh_input["materials"]))
all_end_transforms.extend(copy.deepcopy(mesh_input["transforms"]))
# Package the final outputs
start_frame_combined = {
"meshes": all_start_meshes,
"materials": all_start_materials,
"transforms": all_start_transforms
}
end_frame_combined = {
"meshes": all_end_meshes,
"materials": all_end_materials,
"transforms": all_end_transforms
}
# This is the final sequence object sent to the Scene Builder
mesh_sequence = {
"start_meshes": start_frame_combined,
"end_meshes": end_frame_combined
}
return (start_frame_combined, mesh_sequence)
class RenderFormerMeshTarget:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"start_mesh": ("MESH",),
"end_mesh_pos_x": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.001}),
"end_mesh_pos_y": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.001}),
"end_mesh_pos_z": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 10.0, "step": 0.001}),
"end_mesh_rot_x": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 0.1}),
"end_mesh_rot_y": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 0.1}),
"end_mesh_rot_z": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 0.1}),
"end_scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.01}),
"end_diffuse_rgb": ("STRING", {"default": "204, 204, 204", "multiline": False}),
"end_specular_rgb": ("STRING", {"default": "25, 25, 25", "multiline": False}),
"end_roughness": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("MESH_SEQUENCE",)
FUNCTION = "get_mesh_sequence"
CATEGORY = "PHRenderFormer"
def _parse_color_string(self, color_str, scale=1.0):
if not isinstance(color_str, str): return None
try:
parts = [float(p.strip()) for p in color_str.split(',')]
if len(parts) == 3: return [p / scale for p in parts]
except (ValueError, AttributeError): pass
return None
def get_mesh_sequence(self, start_mesh, end_mesh_pos_x, end_mesh_pos_y, end_mesh_pos_z,
end_mesh_rot_x, end_mesh_rot_y, end_mesh_rot_z, end_scale,
end_diffuse_rgb, end_specular_rgb, end_roughness):
if not start_mesh or "meshes" not in start_mesh or not start_mesh["meshes"]:
raise Exception("RenderFormerMeshTarget: Invalid start_mesh input.")
# --- Create Start and End Data Structures ---
start_meshes_data = []
end_meshes_data = []
# The input `start_mesh` can contain multiple sub-meshes. We need to create a
# start/end pair for each one.
for i in range(len(start_mesh["meshes"])):
# --- Start State ---
# We create a new dictionary for each mesh to ensure they are independent.
start_data = {
"mesh": start_mesh["meshes"][i],
"material": start_mesh["materials"][i],
"transform": start_mesh["transforms"][i],
"ph_uuid": str(uuid.uuid4()) # Add a unique ID for tracking in the combine node
}
start_meshes_data.append(start_data)
# --- End State ---
# Create a deep copy of the start state to modify for the end frame.
end_data = copy.deepcopy(start_data)
# Update transform for the end state
end_data["transform"]["translation"] = [end_mesh_pos_x, end_mesh_pos_y, end_mesh_pos_z]
end_data["transform"]["rotation"] = [end_mesh_rot_x, end_mesh_rot_y, end_mesh_rot_z]
end_data["transform"]["scale"] = [end_scale, end_scale, end_scale]
# Update material for the end state
end_diffuse = self._parse_color_string(end_diffuse_rgb, 255.0)
if end_diffuse:
end_data["material"]["diffuse"] = end_diffuse
end_specular = self._parse_color_string(end_specular_rgb, 255.0)
if end_specular:
end_data["material"]["specular"] = end_specular
end_data["material"]["roughness"] = end_roughness
end_meshes_data.append(end_data)
# Convert the lists of dicts into the final MESH dictionary format
start_frame_combined = {
"meshes": [d["mesh"] for d in start_meshes_data],
"materials": [d["material"] for d in start_meshes_data],
"transforms": [d["transform"] for d in start_meshes_data]
}
end_frame_combined = {
"meshes": [d["mesh"] for d in end_meshes_data],
"materials": [d["material"] for d in end_meshes_data],
"transforms": [d["transform"] for d in end_meshes_data]
}
# The output is a dictionary containing both start and end states,
# with each state being a MESH-formatted dictionary.
mesh_output = {
"start_meshes": start_frame_combined,
"end_meshes": end_frame_combined
}
return (mesh_output,)
class RenderFormerGenerator:
@@ -1580,6 +1758,7 @@ NODE_CLASS_MAPPINGS = {
"RenderFormerFromJSON": RenderFormerFromJSON,
"RenderFormerMeshCombine": RenderFormerMeshCombine,
"RenderFormerLightingCombine": RenderFormerLightingCombine,
"RenderFormerMeshTarget": RenderFormerMeshTarget,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -1597,4 +1776,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"RenderFormerFromJSON": "RenderFormer From JSON",
"RenderFormerMeshCombine": "RenderFormer Mesh Combine",
"RenderFormerLightingCombine": "RenderFormer Lighting Combine",
"RenderFormerMeshTarget": "RenderFormer Mesh Target",
}
+1
View File
@@ -92,6 +92,7 @@ app.registerExtension({
"RenderFormerMeshCombine",
"RenderFormerLightingCombine",
"RenderFormerLightingTarget",
"RenderFormerMeshTarget",
];
if (node_names.includes(node.comfyClass)) {