Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b5bff9222 | |||
| 862849c032 | |||
| e060186716 | |||
| 07c4dd501f | |||
| e80c0c034d | |||
| f40b2faacb | |||
| d2b98716ff | |||
| e4f3cdbf17 | |||
| 1d34ac2dd8 |
@@ -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",
|
||||||
|
|||||||
+19
-5
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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):
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ 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
|
# ["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
|
is_checking_for_update: bool = False
|
||||||
update_needed: bool = False
|
update_needed: bool = False
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,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,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,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에 보고해 주세요.",
|
||||||
|
|||||||
Reference in New Issue
Block a user