diff --git a/core/mmd/core/camera.py b/core/mmd/core/camera.py index 3c6da6e..824b71f 100644 --- a/core/mmd/core/camera.py +++ b/core/mmd/core/camera.py @@ -10,6 +10,7 @@ from typing import Optional, List, Tuple, Callable, Any, Union import bpy from bpy.types import Object, ID, Camera, Context +from bpy_extras import anim_utils from mathutils import Vector, Matrix, Euler import traceback @@ -247,14 +248,27 @@ class MMDCamera: frame_count = frame_end - frame_start frames = range(frame_start, frame_end) + # Get channelbags for camera actions using Blender 5.0 API + if not parent_action.slots: + parent_slot = parent_action.slots.new(for_id=mmd_cam_root) + else: + parent_slot = parent_action.slots[0] + parent_channelbag = anim_utils.action_ensure_channelbag_for_slot(parent_action, parent_slot) + + if not distance_action.slots: + distance_slot = distance_action.slots.new(for_id=mmd_cam) + else: + distance_slot = distance_action.slots[0] + distance_channelbag = anim_utils.action_ensure_channelbag_for_slot(distance_action, distance_slot) + fcurves = [] for i in range(3): - fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z + fcurves.append(parent_channelbag.fcurves.new(data_path="location", index=i)) # x, y, z for i in range(3): - fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz - fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov - fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp - fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis + fcurves.append(parent_channelbag.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz + fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.angle")) # fov + fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.is_perspective")) # persp + fcurves.append(distance_channelbag.fcurves.new(data_path="location", index=1)) # dis for c in fcurves: c.keyframe_points.add(frame_count) diff --git a/core/mmd/core/material.py b/core/mmd/core/material.py index 6706e7e..e61d478 100644 --- a/core/mmd/core/material.py +++ b/core/mmd/core/material.py @@ -481,7 +481,7 @@ class FnMaterial: preferred_output_node_target = { "CYCLES": "CYCLES", - "BLENDER_EEVEE_NEXT": "EEVEE", + "BLENDER_EEVEE": "EEVEE", }.get(active_render_engine, "ALL") tex_node = None @@ -559,7 +559,12 @@ class FnMaterial: mat = self.material if mat.node_tree is None: logger.debug(f"Creating node tree for {mat.name}") - mat.use_nodes = True + # Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes + # Creating a new material automatically creates a node tree + if mat.node_tree is None: + # Fallback: node tree should exist, but if not, log warning + logger.warning(f"Node tree is None for material {mat.name} - this should not happen") + return mat.node_tree.nodes.clear() nodes, links = mat.node_tree.nodes, mat.node_tree.links diff --git a/core/mmd/core/pmx/importer.py b/core/mmd/core/pmx/importer.py index bb3a2cb..5e94b89 100644 --- a/core/mmd/core/pmx/importer.py +++ b/core/mmd/core/pmx/importer.py @@ -534,7 +534,8 @@ class PMXImporter: elif b_bone.name in specialTipBones: mmd_bone.is_tip = True - b_bone.bone.hide = not pmx_bone.visible # or mmd_bone.is_tip + # Blender 5.0: use pose bone hide for Pose/Object mode visibility + b_bone.hide = not pmx_bone.visible # or mmd_bone.is_tip if not pmx_bone.isRotatable: b_bone.lock_rotation = [True, True, True] diff --git a/core/mmd/core/rigid_body.py b/core/mmd/core/rigid_body.py index edb0de5..4f6f5f3 100644 --- a/core/mmd/core/rigid_body.py +++ b/core/mmd/core/rigid_body.py @@ -82,7 +82,7 @@ class RigidBodyMaterial: mat.shadow_method = "NONE" mat.use_backface_culling = True mat.show_transparent_back = False - mat.use_nodes = True + # Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes nodes, links = mat.node_tree.nodes, mat.node_tree.links nodes.clear() node_color = nodes.new("ShaderNodeBackground") diff --git a/core/mmd/core/vmd/importer.py b/core/mmd/core/vmd/importer.py index 07eb925..0c865ef 100644 --- a/core/mmd/core/vmd/importer.py +++ b/core/mmd/core/vmd/importer.py @@ -11,6 +11,7 @@ import os from typing import Union import bpy +from bpy_extras import anim_utils from mathutils import Quaternion, Vector from ... import utils @@ -300,21 +301,31 @@ class VMDImporter: kp.handle_right = kp.co + Vector((1, 0)) @staticmethod - def __keyframe_insert_inner(fcurves: bpy.types.ActionFCurves, path: str, index: int, frame: float, value: float): - fcurve = fcurves.find(path, index=index) + def __get_channelbag(action: bpy.types.Action, target_id=None): + """Get or create channelbag for action using Blender 5.0 API.""" + if not action.slots: + slot = action.slots.new(for_id=target_id) + else: + slot = action.slots[0] + return anim_utils.action_ensure_channelbag_for_slot(action, slot) + + @staticmethod + def __keyframe_insert_inner(action: bpy.types.Action, path: str, index: int, frame: float, value: float, target_id=None, group_name=None): + channelbag = VMDImporter.__get_channelbag(action, target_id) + fcurve = channelbag.fcurves.find(path, index=index) if fcurve is None: - fcurve = fcurves.new(path, index=index) + fcurve = channelbag.fcurves.new(path, index=index, group_name=group_name) fcurve.keyframe_points.insert(frame, value, options={"FAST"}) @staticmethod - def __keyframe_insert(fcurves: bpy.types.ActionFCurves, path: str, frame: float, value: Union[int, float, Vector]): + def __keyframe_insert(action: bpy.types.Action, path: str, frame: float, value: Union[int, float, Vector], target_id=None, group_name=None): if isinstance(value, (int, float)): - VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value) + VMDImporter.__keyframe_insert_inner(action, path, 0, frame, value, target_id, group_name) elif isinstance(value, Vector): - VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value[0]) - VMDImporter.__keyframe_insert_inner(fcurves, path, 1, frame, value[1]) - VMDImporter.__keyframe_insert_inner(fcurves, path, 2, frame, value[2]) + VMDImporter.__keyframe_insert_inner(action, path, 0, frame, value[0], target_id, group_name) + VMDImporter.__keyframe_insert_inner(action, path, 1, frame, value[1], target_id, group_name) + VMDImporter.__keyframe_insert_inner(action, path, 2, frame, value[2], target_id, group_name) else: raise TypeError("Unsupported type: {0}".format(type(value))) @@ -407,16 +418,19 @@ class VMDImporter: assert bone_name_table.get(bone.name, name) == name bone_name_table[bone.name] = name + # Get channelbag for this action + channelbag = self.__get_channelbag(action, armObj.data) + fcurves = [dummy_keyframe_points] * 7 # x, y, z, r0, r1, r2, (r3) data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler") bone_rotation = getattr(bone, data_path_rot) default_values = list(bone.location) + list(bone_rotation) data_path = 'pose.bones["%s"].location' % bone.name for axis_i in range(3): - fcurves[axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) + fcurves[axis_i] = channelbag.fcurves.new(data_path=data_path, index=axis_i, group_name=bone.name) data_path = 'pose.bones["%s"].%s' % (bone.name, data_path_rot) for axis_i in range(len(bone_rotation)): - fcurves[3 + axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) + fcurves[3 + axis_i] = channelbag.fcurves.new(data_path=data_path, index=axis_i, group_name=bone.name) for i in range(len(default_values)): c = fcurves[i] @@ -458,7 +472,9 @@ class VMDImporter: self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp) prev_kps = curr_kps - for c in action.fcurves: + # Get channelbag to iterate fcurves + channelbag = self.__get_channelbag(action, armObj.data) + for c in channelbag.fcurves: self.__fixFcurveHandles(c) # property animation @@ -518,7 +534,8 @@ class VMDImporter: continue logging.info("(mesh) frames:%5d name: %s", len(keyFrames), name) shapeKey = shapeKeyDict[name] - fcurve = action.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name) + channelbag = self.__get_channelbag(action, meshObj.data.shape_keys) + fcurve = channelbag.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name) fcurve.keyframe_points.add(len(keyFrames)) keyFrames.sort(key=lambda x: x.frame_number) for k, v in zip(keyFrames, fcurve.keyframe_points): @@ -541,7 +558,7 @@ class VMDImporter: logging.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim]) for keyFrame in propertyAnim: - self.__keyframe_insert(action.fcurves, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible)) + self.__keyframe_insert(action, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible), rootObj) self.__assign_action(rootObj, action) @@ -574,14 +591,18 @@ class VMDImporter: if self.__mirror: _loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation3 + # Get channelbags for camera actions + parent_channelbag = self.__get_channelbag(parent_action, mmdCamera.parent) + distance_channelbag = self.__get_channelbag(distance_action, mmdCamera.distance) + fcurves = [] for i in range(3): - fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z + fcurves.append(parent_channelbag.fcurves.new(data_path="location", index=i)) # x, y, z for i in range(3): - fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz - fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov - fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp - fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis + fcurves.append(parent_channelbag.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz + fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.angle")) # fov + fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.is_perspective")) # persp + fcurves.append(distance_channelbag.fcurves.new(data_path="location", index=1)) # dis for c in fcurves: c.keyframe_points.add(len(cameraAnim)) @@ -640,10 +661,11 @@ class VMDImporter: _loc = _MirrorMapper.get_location if self.__mirror else lambda i: i for keyFrame in lampAnim: frame = keyFrame.frame_number + self.__frame_margin - self.__keyframe_insert(color_action.fcurves, "color", frame, Vector(keyFrame.color)) - self.__keyframe_insert(location_action.fcurves, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1) + self.__keyframe_insert(color_action, "color", frame, Vector(keyFrame.color), lampObj) + self.__keyframe_insert(location_action, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1, mmdLamp) - for fcurve in location_action.fcurves: + location_channelbag = self.__get_channelbag(location_action, mmdLamp) + for fcurve in location_channelbag.fcurves: self.detectLampChange(fcurve) self.__assign_action(lampObj.data, color_action) diff --git a/core/mmd/cycles_converter.py b/core/mmd/cycles_converter.py index 5f10140..3fb5471 100644 --- a/core/mmd/cycles_converter.py +++ b/core/mmd/cycles_converter.py @@ -127,9 +127,9 @@ def convertToBlenderShader(obj: bpy.types.Object, use_principled: bool = False, for i in obj.material_slots: if not i.material: continue - if not i.material.use_nodes: - logger.debug(f"Enabling nodes for material: {i.material.name}") - i.material.use_nodes = True + # Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes + if not i.material.node_tree or len(i.material.node_tree.nodes) == 0: + logger.debug(f"Setting up node tree for material: {i.material.name}") __convertToMMDBasicShader(i.material) if use_principled: logger.debug(f"Converting material to Principled BSDF: {i.material.name}") @@ -143,9 +143,7 @@ def convertToMMDShader(obj: bpy.types.Object) -> None: for i in obj.material_slots: if not i.material: continue - if not i.material.use_nodes: - logger.debug(f"Enabling nodes for material: {i.material.name}") - i.material.use_nodes = True + # Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes FnMaterial.convert_to_mmd_material(i.material) def __convertToMMDBasicShader(material: Material) -> None: diff --git a/core/mmd/operators/material.py b/core/mmd/operators/material.py index caa76fe..efaaf97 100644 --- a/core/mmd/operators/material.py +++ b/core/mmd/operators/material.py @@ -386,7 +386,7 @@ class EdgePreviewSetup(Operator): return mat def __make_shader(self, m: Material) -> None: - m.use_nodes = True + # Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes nodes, links = m.node_tree.nodes, m.node_tree.links node_shader = nodes.get("mmd_edge_preview", None) diff --git a/core/mmd/operators/morph.py b/core/mmd/operators/morph.py index 1201659..c58b317 100644 --- a/core/mmd/operators/morph.py +++ b/core/mmd/operators/morph.py @@ -475,7 +475,8 @@ class ViewBoneMorph(bpy.types.Operator): for morph_data in morph.data: p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None) if p_bone: - p_bone.bone.select = True + # Blender 5.0: use pose bone select property directly + p_bone.select = True mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4() mtx.translation = p_bone.location + morph_data.location p_bone.matrix_basis = mtx @@ -521,9 +522,10 @@ class ApplyBoneMorph(bpy.types.Operator): item.bone = p_bone.name item.location = p_bone.location item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() - p_bone.bone.select = True + # Blender 5.0: use pose bone select property directly + p_bone.select = True else: - p_bone.bone.select = False + p_bone.select = False logger.info(f"Applied current pose to bone morph: {morph.name}") return {"FINISHED"} diff --git a/core/mmd/operators/view.py b/core/mmd/operators/view.py index 3e82cf4..d9779c3 100644 --- a/core/mmd/operators/view.py +++ b/core/mmd/operators/view.py @@ -32,16 +32,14 @@ class _SetShadingBase: @staticmethod def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None: - for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"): - for s in i.material_slots: - if s.material is None: - continue - s.material.use_nodes = False - s.material.use_shadeless = use_shadeless + # Note: material.use_nodes and material.use_shadeless are deprecated in Blender 5.0 + # Materials always use nodes now, and shadeless is handled differently + # This method is kept for compatibility but no longer modifies materials + pass def execute(self, context: Context) -> Dict[str, str]: - context.scene.render.engine = "BLENDER_EEVEE_NEXT" - logger.debug(f"Setting render engine to BLENDER_EEVEE_NEXT") + context.scene.render.engine = "BLENDER_EEVEE" + logger.debug(f"Setting render engine to BLENDER_EEVEE") shading_mode: Optional[str] = getattr(self, "_shading_mode", None) for space in self._get_view3d_spaces(context): diff --git a/core/mmd/utils.py b/core/mmd/utils.py index 6d6f731..4bfb478 100644 --- a/core/mmd/utils.py +++ b/core/mmd/utils.py @@ -55,14 +55,13 @@ def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: st if reset_pose: for p_bone in armature.pose.bones: p_bone.matrix_basis.identity() + # Blender 5.0: bone selection in Pose mode now uses pose.bones[].select armature_bones: bpy.types.ArmatureBones = armature.data.bones - i: Bone - for i in armature_bones: - i.select = i.name == bone_name - i.select_head = i.select_tail = i.select - if i.select: - armature_bones.active = i - i.hide = False + for p_bone in armature.pose.bones: + p_bone.select = p_bone.name == bone_name + if p_bone.select: + armature_bones.active = armature_bones[p_bone.name] + p_bone.hide = False __CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$") diff --git a/core/resonite_utils.py b/core/resonite_utils.py index d22504d..b1b6333 100644 --- a/core/resonite_utils.py +++ b/core/resonite_utils.py @@ -2,6 +2,7 @@ import traceback from types import FrameType import bpy import bpy_extras +from bpy_extras import anim_utils from numpy import double from typing import Set, Dict import re @@ -115,12 +116,33 @@ class AvatarToolkit_OT_ConvertResonite(Operator): return {'FINISHED'} -def makeorexistingfcurve(action: bpy.types.Action,data_path: str,action_group: str, index=0) -> bpy.types.FCurve: - fcurve = action.fcurves.find(data_path=data_path,index=index) - if fcurve == None: - return action.fcurves.new(data_path,action_group=action_group,index=index) +def makeorexistingfcurve(action: bpy.types.Action, data_path: str, action_group: str, index=0) -> bpy.types.FCurve: + """Get or create an F-Curve using Blender 5.0 channelbag system. + + Blender 5.0 Breaking Change: The legacy action.fcurves API has been removed. + F-Curves are now accessed through channelbags. Each slot of an Action can have a channelbag. + This function has been migrated to use bpy_extras.anim_utils.action_ensure_channelbag_for_slot(). + """ + # Get the action slot (assumes single slot for now - armature actions typically use first slot) + if not action.slots: + slot = action.slots.new(for_id=bpy.context.object.data if bpy.context.object and bpy.context.object.type == 'ARMATURE' else None) else: - print("fcurve with data \""+data_path+"\" already exists") + slot = action.slots[0] + + # Get or create channelbag for this slot + channelbag = anim_utils.action_ensure_channelbag_for_slot(action, slot) + + # Use ensure() to get existing or create new F-Curve + fcurve = channelbag.fcurves.ensure(data_path, index=index, group_name=action_group) + + if fcurve: + return fcurve + else: + print(f"fcurve with data \"{data_path}\" creation failed") + # Fallback: try to find or create manually + fcurve = channelbag.fcurves.find(data_path, index=index) + if fcurve is None: + fcurve = channelbag.fcurves.new(data_path, index=index, group_name=action_group) return fcurve class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper): diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py index 2595238..c8db61c 100644 --- a/functions/atlas_materials.py +++ b/functions/atlas_materials.py @@ -241,7 +241,7 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): # Create material nodes atlased_mat.material = bpy.data.materials.new( name=f"Atlas_Final_{context.scene.name}_{Path(bpy.data.filepath).stem}") - atlased_mat.material.use_nodes = True + # Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes atlased_mat.material.node_tree.nodes.clear() principled_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") diff --git a/functions/tools/bone_tools.py b/functions/tools/bone_tools.py index b185dc3..8ccefc5 100644 --- a/functions/tools/bone_tools.py +++ b/functions/tools/bone_tools.py @@ -1,5 +1,7 @@ import traceback import bpy +import bpy_extras +from bpy_extras import anim_utils import re from bpy.types import Operator, Context, EditBone, Object, Armature, Mesh from typing import Optional, Dict, Any, List, Tuple @@ -347,10 +349,17 @@ class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator): armature_data.bones.foreach_set("select", [False] * len(armature_data.bones)) + # Get channelbag for the action using Blender 5.0 API + action = armature.animation_data.action + if not action.slots: + slot = action.slots.new(for_id=armature.data) + else: + slot = action.slots[0] + channelbag = anim_utils.action_ensure_channelbag_for_slot(action, slot) #create a set for every frame time where we need to key a keyframe for the flipped pose times: Dict[float,list[bpy.types.FCurve]] = {} - for curve in armature.animation_data.action.fcurves: + for curve in channelbag.fcurves: if not curve.data_path.startswith("pose"): continue for point in curve.keyframe_points: