Compare commits

..

22 Commits

Author SHA1 Message Date
Yusarina 0b5bff9222 Update ko_KR.json 2025-11-15 17:21:04 +00:00
Yusarina 862849c032 Update AvatarToolkit label to version 0.5.0 2025-11-15 17:20:56 +00:00
Yusarina e060186716 Update Avatar Toolkit version to Alpha 0.5.0 2025-11-15 17:20:44 +00:00
Onan Chew 07c4dd501f Merge pull request #202 from Yusarina/Current
Migrate to Blender 5.0 API
2025-11-14 22:47:30 -05:00
Yusarina e80c0c034d Version Change
- Min Blender version is 5.0
- ATK version is 0.5.0
2025-11-15 02:52:04 +00:00
Yusarina f40b2faacb Migrate to Blender 5.0 API
- Replaced action.fcurves with channelbag system
- Updated EEVEE_NEXT to EEVEE render engine
- Removed deprecated material.use_nodes and use_shadeless
- Fixed bone selection/hide API for Pose mode
2025-11-15 02:45:37 +00:00
Onan Chew d2b98716ff Merge pull request #201 from Yusarina/Current
Fix texture atlas crash caused by premature image removal
2025-11-12 15:20:01 -05:00
Yusarina e4f3cdbf17 Fix texture atlas crash caused by premature image removal
- Changed image replacement logic to reuse existing placeholder images instead of deleting and recreating them. This should prevents ReferenceError when multiple materials reference the same replacement image
2025-11-12 16:44:07 +00:00
Onan Chew 1d34ac2dd8 Merge pull request #200 from teamneoneko/Alpha-4
Alpha 4
Unbreak things
2025-10-29 12:15:05 -04:00
Onan Chew 3bb533ff64 Merge branch 'Current' into Alpha-4 2025-10-29 12:14:38 -04:00
Onan Chew 69cae02160 Merge pull request #199 from Yusarina/patch-1
Fix updater for Alpha 4 releases
2025-10-29 12:03:39 -04:00
Onan Chew 5496078a39 Merge pull request #197 from Yusarina/Current
Revise README for Blender support and wiki notice
2025-10-29 12:03:18 -04:00
Yusarina dbf2fb77f9 Fix updater for Alpha 4 releases
The tag never for updated for Alpha 4, but also the first release of alpha 4 tag was incorrect so this allows for Alpha 3 and 4 tags.
2025-10-29 15:29:43 +00:00
Yusarina 3de600cf64 Revise README for Blender support and wiki notice
Updated README to reflect changes in Blender version support and added a notice about wiki status and Offical blender version support only.
2025-10-14 17:21:33 +01:00
Onan Chew ba9d579176 Merge pull request #195 from hanzcvr/feat/morebones
dictionaries: add mappings for upper legs and toes observed on Komado's Rusk
2025-10-13 00:22:02 -04:00
Hanz 35458f9aed dictionaries: add mappings for upper legs and toes observed on Komado's Rusk 2025-10-12 19:30:30 -05:00
Onan Chew d2c30caef5 Merge pull request #194 from teamneoneko/Alpha-3
Alpha 3 fix
2025-10-06 19:28:29 -04:00
Onan Chew b9f7a4acd0 Merge pull request #192 from Yusarina/Current
- bug fixes to merge armature button
2025-08-24 16:09:00 -04:00
Yusarina e626bdc5c5 Loggin Fix 2025-08-23 22:36:59 +01:00
Yusarina da2bfeb2fc Version Bump 2025-08-22 23:06:27 +01:00
Yusarina 2b53146e83 Armature Meging Fixes
- Fixes issue with Armature Merging giving a error.
- Fixes logger error
2025-08-22 23:05:05 +01:00
Onan Chew 4ba594d712 Merge pull request #187 from Yusarina/Current
Alpha 3: Avatar Toolkit 0.3.2
2025-08-09 02:47:57 -04:00
21 changed files with 201 additions and 103 deletions
+11 -1
View File
@@ -1,4 +1,5 @@
# Avatar Toolkit # Avatar Toolkit
We are aware the wiki is down and are working on a new one, please don't report this.
## Avatar Toolkit is in Alpha, There will be issues, please ensure you report them!. If using a Alpha plugin isn't your fancy you can find Cats Blender Plugin [HERE](https://github.com/unofficalcats/Cats-Blender-Plugin-Unofficial-)! ## Avatar Toolkit is in Alpha, There will be issues, please ensure you report them!. If using a Alpha plugin isn't your fancy you can find Cats Blender Plugin [HERE](https://github.com/unofficalcats/Cats-Blender-Plugin-Unofficial-)!
#### Avatar Toolkit is in Alpha and will contain issues, please ensure you report them! #### Avatar Toolkit is in Alpha and will contain issues, please ensure you report them!
@@ -35,7 +36,6 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/lega
- Blender 4.5 or newer is required - Blender 4.5 or newer is required
- Blender 4.5 is the current recommended version - Blender 4.5 is the current recommended version
2) Python Requirements 2) Python Requirements
- If using a custom Python installation with Blender, ensure NumPy is installed - If using a custom Python installation with Blender, ensure NumPy is installed
- Default Blender installation includes all required packages - Default Blender installation includes all required packages
@@ -44,6 +44,16 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/lega
- Download Blender directly from https://blender.org - Download Blender directly from https://blender.org
- Use Blender 4.5 for the best experience - Use Blender 4.5 for the best experience
#### Unfortunately, due to the increased number of people complaining to me (yes, we get DMs about this) that AT or CATS is broken when it's not, we are going to have to be a bit more strict about which Blender releases we will provide support for.
#### We only support the following Blender releases:
- Steam release
- The Blender website releases (there are downloads for Linux, Mac, and Windows)
#### We do not support the following what so ever and we will not give help if your running the following.
- We do not support the Windows Store due to it causing issues, and we also don't support the Snap Store for Linux.
- We do not support package manager releases on Linux. This is because package managers are normally run by the distro, and a lot of the time the distro will build Blender themselves and make their own changes which are not sanctioned by Blender (for example, bundling a newer version of Python which tends to break plugins). If you report a bug from anything apart from the Blender versions we support, you will be told we can't help you from now on.
#### Additional Plugins Requirements. #### Additional Plugins Requirements.
Currently None. Currently None.
+2 -2
View File
@@ -3,13 +3,13 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "avatar_toolkit" id = "avatar_toolkit"
version = "0.4.0" version = "0.5.0"
name = "Avatar Toolkit" name = "Avatar Toolkit"
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
maintainer = "Team NekoNeo" maintainer = "Team NekoNeo"
type = "add-on" type = "add-on"
blender_version_min = "4.5.0" blender_version_min = "5.0.0"
license = [ license = [
"SPDX:GPL-3.0-or-later", "SPDX:GPL-3.0-or-later",
+6 -4
View File
@@ -881,7 +881,8 @@ non_standard_mappings = {
'left_leg': [ 'left_leg': [
'mixamorig:LeftUpLeg', 'mixamorig_LeftUpLeg', 'mixamorig:LeftUpLeg', 'mixamorig_LeftUpLeg',
'ORG-thigh.L', 'thigh.L', 'ORG-thigh.L', 'thigh.L',
'lThighBend', 'lThigh', 'UpperLeg.L' 'lThighBend', 'lThigh', 'UpperLeg.L',
'LeftUpperLeg'
], ],
'left_knee': [ 'left_knee': [
'mixamorig:LeftLeg', 'mixamorig_LeftLeg', 'mixamorig:LeftLeg', 'mixamorig_LeftLeg',
@@ -896,13 +897,14 @@ non_standard_mappings = {
'left_toe': [ 'left_toe': [
'mixamorig:LeftToeBase', 'mixamorig_LeftToeBase', 'mixamorig:LeftToeBase', 'mixamorig_LeftToeBase',
'ORG-toe.L', 'toe.L', 'ORG-toe.L', 'toe.L',
'lToe', 'Toes.L' 'lToe', 'Toes.L', 'LeftToeBase'
], ],
'right_leg': [ 'right_leg': [
'mixamorig:RightUpLeg', 'mixamorig_RightUpLeg', 'mixamorig:RightUpLeg', 'mixamorig_RightUpLeg',
'ORG-thigh.R', 'thigh.R', 'ORG-thigh.R', 'thigh.R',
'rThighBend', 'rThigh', 'UpperLeg.R' 'rThighBend', 'rThigh', 'UpperLeg.R',
'RightUpperLeg'
], ],
'right_knee': [ 'right_knee': [
'mixamorig:RightLeg', 'mixamorig_RightLeg', 'mixamorig:RightLeg', 'mixamorig_RightLeg',
@@ -917,7 +919,7 @@ non_standard_mappings = {
'right_toe': [ 'right_toe': [
'mixamorig:RightToeBase', 'mixamorig_RightToeBase', 'mixamorig:RightToeBase', 'mixamorig_RightToeBase',
'ORG-toe.R', 'toe.R', 'ORG-toe.R', 'toe.R',
'rToe', 'Toes.R' 'rToe', 'Toes.R', 'RightToeBase'
], ],
'thumb_1_l': [ 'thumb_1_l': [
+19 -5
View File
@@ -10,6 +10,7 @@ from typing import Optional, List, Tuple, Callable, Any, Union
import bpy import bpy
from bpy.types import Object, ID, Camera, Context from bpy.types import Object, ID, Camera, Context
from bpy_extras import anim_utils
from mathutils import Vector, Matrix, Euler from mathutils import Vector, Matrix, Euler
import traceback import traceback
@@ -247,14 +248,27 @@ class MMDCamera:
frame_count = frame_end - frame_start frame_count = frame_end - frame_start
frames = range(frame_start, frame_end) 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 = [] fcurves = []
for i in range(3): 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): for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz fcurves.append(parent_channelbag.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_channelbag.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis fcurves.append(distance_channelbag.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves: for c in fcurves:
c.keyframe_points.add(frame_count) c.keyframe_points.add(frame_count)
+7 -2
View File
@@ -481,7 +481,7 @@ class FnMaterial:
preferred_output_node_target = { preferred_output_node_target = {
"CYCLES": "CYCLES", "CYCLES": "CYCLES",
"BLENDER_EEVEE_NEXT": "EEVEE", "BLENDER_EEVEE": "EEVEE",
}.get(active_render_engine, "ALL") }.get(active_render_engine, "ALL")
tex_node = None tex_node = None
@@ -559,7 +559,12 @@ class FnMaterial:
mat = self.material mat = self.material
if mat.node_tree is None: if mat.node_tree is None:
logger.debug(f"Creating node tree for {mat.name}") 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() mat.node_tree.nodes.clear()
nodes, links = mat.node_tree.nodes, mat.node_tree.links nodes, links = mat.node_tree.nodes, mat.node_tree.links
+2 -1
View File
@@ -534,7 +534,8 @@ class PMXImporter:
elif b_bone.name in specialTipBones: elif b_bone.name in specialTipBones:
mmd_bone.is_tip = True 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: if not pmx_bone.isRotatable:
b_bone.lock_rotation = [True, True, True] b_bone.lock_rotation = [True, True, True]
+1 -1
View File
@@ -82,7 +82,7 @@ class RigidBodyMaterial:
mat.shadow_method = "NONE" mat.shadow_method = "NONE"
mat.use_backface_culling = True mat.use_backface_culling = True
mat.show_transparent_back = False 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, links = mat.node_tree.nodes, mat.node_tree.links
nodes.clear() nodes.clear()
node_color = nodes.new("ShaderNodeBackground") node_color = nodes.new("ShaderNodeBackground")
+43 -21
View File
@@ -11,6 +11,7 @@ import os
from typing import Union from typing import Union
import bpy import bpy
from bpy_extras import anim_utils
from mathutils import Quaternion, Vector from mathutils import Quaternion, Vector
from ... import utils from ... import utils
@@ -300,21 +301,31 @@ class VMDImporter:
kp.handle_right = kp.co + Vector((1, 0)) kp.handle_right = kp.co + Vector((1, 0))
@staticmethod @staticmethod
def __keyframe_insert_inner(fcurves: bpy.types.ActionFCurves, path: str, index: int, frame: float, value: float): def __get_channelbag(action: bpy.types.Action, target_id=None):
fcurve = fcurves.find(path, index=index) """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: 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"}) fcurve.keyframe_points.insert(frame, value, options={"FAST"})
@staticmethod @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)): 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): elif isinstance(value, Vector):
VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value[0]) VMDImporter.__keyframe_insert_inner(action, path, 0, frame, value[0], target_id, group_name)
VMDImporter.__keyframe_insert_inner(fcurves, path, 1, frame, value[1]) VMDImporter.__keyframe_insert_inner(action, path, 1, frame, value[1], target_id, group_name)
VMDImporter.__keyframe_insert_inner(fcurves, path, 2, frame, value[2]) VMDImporter.__keyframe_insert_inner(action, path, 2, frame, value[2], target_id, group_name)
else: else:
raise TypeError("Unsupported type: {0}".format(type(value))) raise TypeError("Unsupported type: {0}".format(type(value)))
@@ -407,16 +418,19 @@ class VMDImporter:
assert bone_name_table.get(bone.name, name) == name assert bone_name_table.get(bone.name, name) == name
bone_name_table[bone.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) fcurves = [dummy_keyframe_points] * 7 # x, y, z, r0, r1, r2, (r3)
data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler") data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler")
bone_rotation = getattr(bone, data_path_rot) bone_rotation = getattr(bone, data_path_rot)
default_values = list(bone.location) + list(bone_rotation) default_values = list(bone.location) + list(bone_rotation)
data_path = 'pose.bones["%s"].location' % bone.name data_path = 'pose.bones["%s"].location' % bone.name
for axis_i in range(3): 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) data_path = 'pose.bones["%s"].%s' % (bone.name, data_path_rot)
for axis_i in range(len(bone_rotation)): 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)): for i in range(len(default_values)):
c = fcurves[i] c = fcurves[i]
@@ -458,7 +472,9 @@ class VMDImporter:
self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp) self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp)
prev_kps = curr_kps 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) self.__fixFcurveHandles(c)
# property animation # property animation
@@ -518,7 +534,8 @@ class VMDImporter:
continue continue
logging.info("(mesh) frames:%5d name: %s", len(keyFrames), name) logging.info("(mesh) frames:%5d name: %s", len(keyFrames), name)
shapeKey = shapeKeyDict[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)) fcurve.keyframe_points.add(len(keyFrames))
keyFrames.sort(key=lambda x: x.frame_number) keyFrames.sort(key=lambda x: x.frame_number)
for k, v in zip(keyFrames, fcurve.keyframe_points): 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]) logging.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim])
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) self.__assign_action(rootObj, action)
@@ -574,14 +591,18 @@ class VMDImporter:
if self.__mirror: if self.__mirror:
_loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation3 _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 = [] fcurves = []
for i in range(3): 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): for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz fcurves.append(parent_channelbag.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_channelbag.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis fcurves.append(distance_channelbag.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves: for c in fcurves:
c.keyframe_points.add(len(cameraAnim)) c.keyframe_points.add(len(cameraAnim))
@@ -640,10 +661,11 @@ class VMDImporter:
_loc = _MirrorMapper.get_location if self.__mirror else lambda i: i _loc = _MirrorMapper.get_location if self.__mirror else lambda i: i
for keyFrame in lampAnim: for keyFrame in lampAnim:
frame = keyFrame.frame_number + self.__frame_margin frame = keyFrame.frame_number + self.__frame_margin
self.__keyframe_insert(color_action.fcurves, "color", frame, Vector(keyFrame.color)) self.__keyframe_insert(color_action, "color", frame, Vector(keyFrame.color), lampObj)
self.__keyframe_insert(location_action.fcurves, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1) 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.detectLampChange(fcurve)
self.__assign_action(lampObj.data, color_action) self.__assign_action(lampObj.data, color_action)
+4 -6
View File
@@ -127,9 +127,9 @@ def convertToBlenderShader(obj: bpy.types.Object, use_principled: bool = False,
for i in obj.material_slots: for i in obj.material_slots:
if not i.material: if not i.material:
continue continue
if not i.material.use_nodes: # Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes
logger.debug(f"Enabling nodes for material: {i.material.name}") if not i.material.node_tree or len(i.material.node_tree.nodes) == 0:
i.material.use_nodes = True logger.debug(f"Setting up node tree for material: {i.material.name}")
__convertToMMDBasicShader(i.material) __convertToMMDBasicShader(i.material)
if use_principled: if use_principled:
logger.debug(f"Converting material to Principled BSDF: {i.material.name}") 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: for i in obj.material_slots:
if not i.material: if not i.material:
continue continue
if not i.material.use_nodes: # Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes
logger.debug(f"Enabling nodes for material: {i.material.name}")
i.material.use_nodes = True
FnMaterial.convert_to_mmd_material(i.material) FnMaterial.convert_to_mmd_material(i.material)
def __convertToMMDBasicShader(material: Material) -> None: def __convertToMMDBasicShader(material: Material) -> None:
+1 -1
View File
@@ -386,7 +386,7 @@ class EdgePreviewSetup(Operator):
return mat return mat
def __make_shader(self, m: Material) -> None: 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 nodes, links = m.node_tree.nodes, m.node_tree.links
node_shader = nodes.get("mmd_edge_preview", None) node_shader = nodes.get("mmd_edge_preview", None)
+5 -3
View File
@@ -475,7 +475,8 @@ class ViewBoneMorph(bpy.types.Operator):
for morph_data in morph.data: for morph_data in morph.data:
p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None) p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None)
if p_bone: 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 = (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 mtx.translation = p_bone.location + morph_data.location
p_bone.matrix_basis = mtx p_bone.matrix_basis = mtx
@@ -521,9 +522,10 @@ class ApplyBoneMorph(bpy.types.Operator):
item.bone = p_bone.name item.bone = p_bone.name
item.location = p_bone.location item.location = p_bone.location
item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() 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: else:
p_bone.bone.select = False p_bone.select = False
logger.info(f"Applied current pose to bone morph: {morph.name}") logger.info(f"Applied current pose to bone morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
+6 -8
View File
@@ -32,16 +32,14 @@ class _SetShadingBase:
@staticmethod @staticmethod
def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None: 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"): # Note: material.use_nodes and material.use_shadeless are deprecated in Blender 5.0
for s in i.material_slots: # Materials always use nodes now, and shadeless is handled differently
if s.material is None: # This method is kept for compatibility but no longer modifies materials
continue pass
s.material.use_nodes = False
s.material.use_shadeless = use_shadeless
def execute(self, context: Context) -> Dict[str, str]: def execute(self, context: Context) -> Dict[str, str]:
context.scene.render.engine = "BLENDER_EEVEE_NEXT" context.scene.render.engine = "BLENDER_EEVEE"
logger.debug(f"Setting render engine to BLENDER_EEVEE_NEXT") logger.debug(f"Setting render engine to BLENDER_EEVEE")
shading_mode: Optional[str] = getattr(self, "_shading_mode", None) shading_mode: Optional[str] = getattr(self, "_shading_mode", None)
for space in self._get_view3d_spaces(context): for space in self._get_view3d_spaces(context):
+6 -7
View File
@@ -55,14 +55,13 @@ def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: st
if reset_pose: if reset_pose:
for p_bone in armature.pose.bones: for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity() 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 armature_bones: bpy.types.ArmatureBones = armature.data.bones
i: Bone for p_bone in armature.pose.bones:
for i in armature_bones: p_bone.select = p_bone.name == bone_name
i.select = i.name == bone_name if p_bone.select:
i.select_head = i.select_tail = i.select armature_bones.active = armature_bones[p_bone.name]
if i.select: p_bone.hide = False
armature_bones.active = i
i.hide = False
__CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$") __CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$")
+27 -5
View File
@@ -2,6 +2,7 @@ import traceback
from types import FrameType from types import FrameType
import bpy import bpy
import bpy_extras import bpy_extras
from bpy_extras import anim_utils
from numpy import double from numpy import double
from typing import Set, Dict from typing import Set, Dict
import re import re
@@ -115,12 +116,33 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
return {'FINISHED'} return {'FINISHED'}
def makeorexistingfcurve(action: bpy.types.Action,data_path: str,action_group: str, index=0) -> bpy.types.FCurve: 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) """Get or create an F-Curve using Blender 5.0 channelbag system.
if fcurve == None:
return action.fcurves.new(data_path,action_group=action_group,index=index) 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: 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 return fcurve
class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper): class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper):
+2 -2
View File
@@ -19,8 +19,8 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
# Define which version series this installation can update to # Define which version series this installation can update to
# For example: ["0.1"] means only look for 0.1.x updates # For example: ["0.1"] means only look for 0.1.x updates
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates # ["0.2", "0.3"] would look for both 0.2.x and 0.3.x
ALLOWED_VERSION_SERIES = ["0.4"] ALLOWED_ = ["0.5"]
is_checking_for_update: bool = False is_checking_for_update: bool = False
update_needed: bool = False update_needed: bool = False
+40 -26
View File
@@ -28,7 +28,15 @@ def scale_images_to_largest(images: List[Image]) -> tuple[int, int]:
x: int = 0 x: int = 0
y: int = 0 y: int = 0
valid_images = [img for img in images if img and img.has_data] valid_images = []
for img in images:
if img:
try:
if img.has_data:
valid_images.append(img)
except ReferenceError:
# Image has been removed from Blender's memory
pass
if not valid_images: if not valid_images:
return 0, 0 return 0, 0
@@ -66,50 +74,56 @@ def get_material_images_from_scene(context: Context) -> list[MaterialImageList]:
new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo] new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo]
except Exception: except Exception:
name = mat_slot.material.name + "_albedo_replacement" name = mat_slot.material.name + "_albedo_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32) else:
new_mat_image_item.albedo = bpy.data.images[name]
try: try:
new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal] new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal]
except Exception: except Exception:
name = mat_slot.material.name + "_normal_replacement" name = mat_slot.material.name + "_normal_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32)
new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32) else:
new_mat_image_item.normal = bpy.data.images[name]
try: try:
new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission] new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission]
except Exception: except Exception:
name = mat_slot.material.name + "_emission_replacement" name = mat_slot.material.name + "_emission_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32) else:
new_mat_image_item.emission = bpy.data.images[name]
try: try:
new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion] new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion]
except Exception: except Exception:
name = mat_slot.material.name + "_ambient_occlusion_replacement" name = mat_slot.material.name + "_ambient_occlusion_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32)
new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32) else:
new_mat_image_item.ambient_occlusion = bpy.data.images[name]
try: try:
new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height] new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height]
except Exception: except Exception:
name = mat_slot.material.name + "_height_replacement" name = mat_slot.material.name + "_height_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32)
new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32) else:
new_mat_image_item.height = bpy.data.images[name]
try: try:
new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness] new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness]
except Exception: except Exception:
name = mat_slot.material.name + "_roughness_replacement" name = mat_slot.material.name + "_roughness_replacement"
if name in bpy.data.images: if name not in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True) new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True) new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32)
new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32) else:
new_mat_image_item.roughness = bpy.data.images[name]
new_mat_image_item.material = mat_slot.material new_mat_image_item.material = mat_slot.material
new_mat_image_item.parent_mesh = obj new_mat_image_item.parent_mesh = obj
@@ -227,7 +241,7 @@ class AvatarToolKit_OT_AtlasMaterials(Operator):
# Create material nodes # Create material nodes
atlased_mat.material = bpy.data.materials.new( atlased_mat.material = bpy.data.materials.new(
name=f"Atlas_Final_{context.scene.name}_{Path(bpy.data.filepath).stem}") 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() atlased_mat.material.node_tree.nodes.clear()
principled_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") principled_node = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
+4 -2
View File
@@ -102,6 +102,7 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
wm.progress_update(100) wm.progress_update(100)
wm.progress_end() wm.progress_end()
# Restore settings only for the base armature since merge_armature is removed during join
restore_breaking_settings_armature(base_armature, data_breaking_base) restore_breaking_settings_armature(base_armature, data_breaking_base)
if merge_armature_name_stored in bpy.data.objects: if merge_armature_name_stored in bpy.data.objects:
merge_armature_obj = bpy.data.objects[merge_armature_name_stored] merge_armature_obj = bpy.data.objects[merge_armature_name_stored]
@@ -124,8 +125,9 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
except Exception as e: except Exception as e:
logger.error(f"Error merging armatures: {str(e)}\n{traceback.format_exc()}") errormessage: str = traceback.format_exc()
self.report({'ERROR'}, traceback.format_exc()) logger.error(f"Error merging armatures: {str(e)}\n{errormessage}")
self.report({'ERROR'}, f"Error merging armatures: {errormessage}")
# Try to restore original mode even on error # Try to restore original mode even on error
try: try:
+10 -1
View File
@@ -1,5 +1,7 @@
import traceback import traceback
import bpy import bpy
import bpy_extras
from bpy_extras import anim_utils
import re import re
from bpy.types import Operator, Context, EditBone, Object, Armature, Mesh from bpy.types import Operator, Context, EditBone, Object, Armature, Mesh
from typing import Optional, Dict, Any, List, Tuple 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)) 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 #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]] = {} 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"): if not curve.data_path.startswith("pose"):
continue continue
for point in curve.keyframe_points: for point in curve.keyframe_points:
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.4.0)", "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.5.0)",
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there", "AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
"AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.", "AvatarToolkit.desc3": "please report it on our Github.",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "アバターツールキット (アルファ 0.4.0)", "AvatarToolkit.label": "アバターツールキット (アルファ 0.5.0)",
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
"AvatarToolkit.desc3": "GitHubで報告してください。", "AvatarToolkit.desc3": "GitHubで報告してください。",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "아바타 툴킷 (알파 0.4.0)", "AvatarToolkit.label": "아바타 툴킷 (알파 0.5.0)",
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
"AvatarToolkit.desc3": "Github에 보고해 주세요.", "AvatarToolkit.desc3": "Github에 보고해 주세요.",