Compare commits

...

8 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
18 changed files with 177 additions and 93 deletions
+2 -2
View File
@@ -3,13 +3,13 @@
schema_version = "1.0.0"
id = "avatar_toolkit"
version = "0.4.0"
version = "0.5.0"
name = "Avatar Toolkit"
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
maintainer = "Team NekoNeo"
type = "add-on"
blender_version_min = "4.5.0"
blender_version_min = "5.0.0"
license = [
"SPDX:GPL-3.0-or-later",
+19 -5
View File
@@ -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)
+7 -2
View File
@@ -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
+2 -1
View File
@@ -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]
+1 -1
View File
@@ -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")
+43 -21
View File
@@ -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)
+4 -6
View File
@@ -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:
+1 -1
View File
@@ -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)
+5 -3
View File
@@ -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"}
+6 -8
View File
@@ -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):
+6 -7
View File
@@ -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("^(.*)左(.*)$")
+27 -5
View File
@@ -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):
+1 -1
View File
@@ -20,7 +20,7 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
# Define which version series this installation can update to
# 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
ALLOWED_ = ["0.3, 0.4"]
ALLOWED_ = ["0.5"]
is_checking_for_update: 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
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:
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]
except Exception:
name = mat_slot.material.name + "_albedo_replacement"
if name 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.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
if name not in bpy.data.images:
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)
else:
new_mat_image_item.albedo = bpy.data.images[name]
try:
new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal]
except Exception:
name = mat_slot.material.name + "_normal_replacement"
if name 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.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32)
if name not in bpy.data.images:
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)
else:
new_mat_image_item.normal = bpy.data.images[name]
try:
new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission]
except Exception:
name = mat_slot.material.name + "_emission_replacement"
if name 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.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
if name not in bpy.data.images:
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)
else:
new_mat_image_item.emission = bpy.data.images[name]
try:
new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion]
except Exception:
name = mat_slot.material.name + "_ambient_occlusion_replacement"
if name 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.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32)
if name not in bpy.data.images:
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)
else:
new_mat_image_item.ambient_occlusion = bpy.data.images[name]
try:
new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height]
except Exception:
name = mat_slot.material.name + "_height_replacement"
if name 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.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32)
if name not in bpy.data.images:
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)
else:
new_mat_image_item.height = bpy.data.images[name]
try:
new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness]
except Exception:
name = mat_slot.material.name + "_roughness_replacement"
if name 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.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32)
if name not in bpy.data.images:
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)
else:
new_mat_image_item.roughness = bpy.data.images[name]
new_mat_image_item.material = mat_slot.material
new_mat_image_item.parent_mesh = obj
@@ -227,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")
+10 -1
View File
@@ -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:
+1 -1
View File
@@ -1,7 +1,7 @@
{
"authors": ["Avatar Toolkit Team"],
"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.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AvatarToolkit.label": "アバターツールキット (アルファ 0.4.0)",
"AvatarToolkit.label": "アバターツールキット (アルファ 0.5.0)",
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
"AvatarToolkit.desc3": "GitHubで報告してください。",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AvatarToolkit.label": "아바타 툴킷 (알파 0.4.0)",
"AvatarToolkit.label": "아바타 툴킷 (알파 0.5.0)",
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
"AvatarToolkit.desc3": "Github에 보고해 주세요.",