feat: Add VRM Blender addon with complete import/export functionality
- Add core VRM addon infrastructure with manifest and registration - Add common utilities module with file system, logging, and conversion helpers - Add human bone mapper with support for multiple rigging standards (Mixamo, MMD, Unreal, Rigify, etc.) - Add VRM 0.x and 1.x format support with property groups and handlers - Add editor UI panels for VRM metadata, spring bones, and MToon materials - Add exporter with glTF2 extension support for VRM format serialization - Add importer with scene reconstruction and armature generation - Add MToon shader support with auto-setup and material migration - Add spring bone physics simulation with constraint handling - Add node constraint editor for advanced rigging control - Add comprehensive validation and error handling with user dialogs - Add scene watcher for real-time property synchronization - Add workspace management and preference system - Include Python cache files and Blender manifest configuration - This is the initial commit establishing the complete VRM addon ecosystem for Blender
This commit is contained in:
377
editor/mtoon1/scene_watcher.py
Normal file
377
editor/mtoon1/scene_watcher.py
Normal file
@@ -0,0 +1,377 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Final, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, Material, Mesh, ShaderNodeGroup, ShaderNodeOutputMaterial
|
||||
|
||||
from ...common import ops
|
||||
from ...common.logger import get_logger
|
||||
from ...common.scene_watcher import RunState, SceneWatcher
|
||||
from ...common.shader import MTOON1_AUTO_SETUP_GROUP_NODE_TREE_CUSTOM_KEY
|
||||
from ..extension import get_material_extension
|
||||
from .ops import VRM_OT_refresh_mtoon1_outline, generate_mtoon1_outline_material_name
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
HAS_AUTO_SMOOTH: Final = tuple(bpy.app.version) < (4, 1)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComparisonMaterial:
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComparisonObject:
|
||||
use_auto_smooth: Optional[bool] = None
|
||||
comparison_materials: list[Optional[ComparisonMaterial]] = field(
|
||||
default_factory=list[Optional[ComparisonMaterial]]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutlineUpdater(SceneWatcher):
|
||||
comparison_objects: list[ComparisonObject] = field(
|
||||
default_factory=list[ComparisonObject]
|
||||
)
|
||||
outline_material_key_to_material_name: dict[int, str] = field(
|
||||
default_factory=dict[int, str]
|
||||
)
|
||||
|
||||
object_index: int = 0
|
||||
comparison_object_index: int = 0
|
||||
material_slot_index: int = 0
|
||||
|
||||
@staticmethod
|
||||
def get_outline_material_key(material: Material) -> int:
|
||||
return material.as_pointer() ^ 0x5F5FF5F5
|
||||
|
||||
def reset_run_progress(self) -> None:
|
||||
self.object_index = 0
|
||||
self.comparison_object_index = 0
|
||||
self.material_slot_index = 0
|
||||
self.outline_material_key_to_material_name.clear()
|
||||
|
||||
def run(self, context: Context) -> RunState:
|
||||
"""Detect changes in material assignments to objects and assign outlines."""
|
||||
blend_data = context.blend_data
|
||||
|
||||
# If this value becomes zero, return PREEMPT and interrupt the process.
|
||||
# If a change is detected, set a virtually infinite value so that the
|
||||
# process proceeds to the end.
|
||||
preempt_countdown = 15
|
||||
|
||||
changed = False
|
||||
|
||||
create_modifier = False
|
||||
|
||||
objects = blend_data.objects
|
||||
if not objects:
|
||||
return RunState.FINISH
|
||||
|
||||
# If the number of objects is less than the previous state and the index
|
||||
# range is exceeded, start over from the beginning
|
||||
objects_len = len(objects)
|
||||
if self.object_index >= objects_len:
|
||||
self.object_index = 0
|
||||
|
||||
# Scan objects and check for differences with the comparison object
|
||||
next_object_index = self.object_index
|
||||
for obj in objects[self.object_index : objects_len]:
|
||||
self.object_index = next_object_index
|
||||
next_object_index += 1
|
||||
|
||||
preempt_countdown -= 1
|
||||
if preempt_countdown <= 0:
|
||||
return RunState.PREEMPT
|
||||
|
||||
# Only mesh objects are subject to investigation.
|
||||
# Skip if it is not a mesh object
|
||||
obj_data = obj.data
|
||||
if not isinstance(obj_data, Mesh):
|
||||
continue
|
||||
mesh = obj_data
|
||||
|
||||
# If the number of comparison objects is insufficient, add a new
|
||||
# comparison object
|
||||
while self.comparison_object_index >= len(self.comparison_objects):
|
||||
self.comparison_objects.append(ComparisonObject())
|
||||
|
||||
# Get a comparison object
|
||||
comparison_object = self.comparison_objects[self.comparison_object_index]
|
||||
|
||||
# Comparison of use_auto_smooth
|
||||
if HAS_AUTO_SMOOTH and (
|
||||
(use_auto_smooth := comparison_object.use_auto_smooth) is None
|
||||
or (use_auto_smooth != mesh.use_auto_smooth)
|
||||
):
|
||||
changed, preempt_countdown = True, sys.maxsize
|
||||
# Resolve change differences
|
||||
comparison_object.use_auto_smooth = mesh.use_auto_smooth
|
||||
|
||||
# If the number of MaterialSlots is less than the previous state and
|
||||
# the index range is exceeded, start over from the beginning
|
||||
material_slots = obj.material_slots
|
||||
material_slots_len = len(material_slots)
|
||||
if self.material_slot_index >= material_slots_len:
|
||||
self.material_slot_index = 0
|
||||
|
||||
# Match the number of MaterialSlots and the number of comparison Materials
|
||||
while material_slots_len > len(comparison_object.comparison_materials):
|
||||
comparison_object.comparison_materials.append(None)
|
||||
while material_slots_len < len(comparison_object.comparison_materials):
|
||||
comparison_object.comparison_materials.pop()
|
||||
|
||||
# Scan MaterialSlots and check for differences with the comparison Material
|
||||
next_material_slot_index = self.material_slot_index
|
||||
for material_slot in material_slots[
|
||||
self.material_slot_index : material_slots_len
|
||||
]:
|
||||
material_slot_index = self.material_slot_index = (
|
||||
next_material_slot_index
|
||||
)
|
||||
next_material_slot_index += 1
|
||||
|
||||
preempt_countdown -= 1
|
||||
if preempt_countdown <= 0:
|
||||
return RunState.PREEMPT
|
||||
|
||||
# If the number of comparison objects is insufficient, add a new
|
||||
# comparison object
|
||||
while material_slot_index >= len(
|
||||
comparison_object.comparison_materials
|
||||
):
|
||||
comparison_object.comparison_materials.append(None)
|
||||
comparison_material = comparison_object.comparison_materials[
|
||||
material_slot_index
|
||||
]
|
||||
|
||||
# Difference check
|
||||
if not (
|
||||
(material := material_slot.material)
|
||||
and (mtoon1 := get_material_extension(material).mtoon1)
|
||||
and mtoon1.get_enabled()
|
||||
):
|
||||
# MToon of the material in the material slot is disabled
|
||||
if comparison_material is None:
|
||||
# No updates
|
||||
continue
|
||||
# if the comparison object is enabled, a change is detected
|
||||
changed, preempt_countdown = True, sys.maxsize
|
||||
# Resolve change differences
|
||||
comparison_object.comparison_materials[material_slot_index] = None
|
||||
continue
|
||||
|
||||
# MToon of the material in the material slot is enabled
|
||||
|
||||
outline_material = mtoon1.outline_material
|
||||
outline_material_name = generate_mtoon1_outline_material_name(material)
|
||||
|
||||
if (
|
||||
comparison_material
|
||||
and comparison_material.name == material.name
|
||||
and (
|
||||
outline_material is None
|
||||
or outline_material.name == outline_material_name
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
# MToon of the material in the material slot is enabled,
|
||||
# but if the comparison object does not exist or the name
|
||||
# does not match, a change is detected
|
||||
changed, preempt_countdown = True, sys.maxsize
|
||||
# Resolve change differences
|
||||
comparison_object.comparison_materials[material_slot_index] = (
|
||||
ComparisonMaterial(material.name)
|
||||
)
|
||||
# A material with MToon enabled has been newly assigned
|
||||
# to the object so, if necessary, create a new outline
|
||||
# modifier.
|
||||
# Originally, True/False should be set for each object and
|
||||
# material pair, but for now, I think it's okay to fix it
|
||||
# for practical use.
|
||||
create_modifier = True
|
||||
|
||||
if outline_material is None:
|
||||
continue
|
||||
|
||||
# When a material is copied, a single outline material may be
|
||||
# shared by multiple MToon materials. To resolve this, copy the
|
||||
# outline material and reassign it.
|
||||
outline_material_key = self.get_outline_material_key(outline_material)
|
||||
original_material_name = self.outline_material_key_to_material_name.get(
|
||||
outline_material_key
|
||||
)
|
||||
if original_material_name is None:
|
||||
self.outline_material_key_to_material_name[outline_material_key] = (
|
||||
material.name
|
||||
)
|
||||
elif original_material_name != material.name:
|
||||
outline_material = outline_material.copy()
|
||||
outline_material_key = self.get_outline_material_key(
|
||||
outline_material
|
||||
)
|
||||
self.outline_material_key_to_material_name[outline_material_key] = (
|
||||
material.name
|
||||
)
|
||||
mtoon1.outline_material = outline_material
|
||||
|
||||
# The outline material name follows the naming changes of
|
||||
# the base MToon material.
|
||||
if outline_material.name != outline_material_name:
|
||||
outline_material.name = outline_material_name
|
||||
|
||||
# Since the scanning of MaterialSlots is complete,
|
||||
# reset the next scanning index to 0.
|
||||
self.material_slot_index = 0
|
||||
|
||||
# Before scanning the next object,
|
||||
# advance the index of the next comparison object.
|
||||
self.comparison_object_index += 1
|
||||
|
||||
# Since the number of elements in self.comparison_objects may be
|
||||
# unnecessarily large, reduce it to a sufficient size
|
||||
while len(self.comparison_objects) > self.comparison_object_index:
|
||||
self.comparison_objects.pop()
|
||||
|
||||
if not changed:
|
||||
return RunState.FINISH
|
||||
|
||||
VRM_OT_refresh_mtoon1_outline.refresh(context, create_modifier=create_modifier)
|
||||
return RunState.FINISH
|
||||
|
||||
def create_fast_path_performance_test_objects(self, context: Context) -> None:
|
||||
blend_data = context.blend_data
|
||||
|
||||
for i in range(100):
|
||||
blend_data.materials.new(f"Material#{i}")
|
||||
|
||||
for i in range(100):
|
||||
mesh = blend_data.meshes.new(f"Mesh#{i}")
|
||||
obj = blend_data.objects.new(f"Object#{i}", mesh)
|
||||
context.scene.collection.objects.link(obj)
|
||||
for k in range(50):
|
||||
material = blend_data.materials[(k * 3) % len(blend_data.materials)]
|
||||
mesh.materials.append(material)
|
||||
if k % 5 == 0:
|
||||
get_material_extension(material).mtoon1.enabled = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class MToon1AutoSetup(SceneWatcher):
|
||||
last_material_index: int = 0
|
||||
last_node_index: int = 0
|
||||
|
||||
def reset_run_progress(self) -> None:
|
||||
self.last_material_index: int = 0
|
||||
self.last_node_index: int = 0
|
||||
|
||||
def run(self, context: Context) -> RunState:
|
||||
"""Monitor the appearance of MToon auto-setup node groups and set them up.
|
||||
|
||||
Since this function is called frequently, keep the processing lightweight and
|
||||
be careful to minimize IO and GC Allocation.
|
||||
"""
|
||||
# If this value becomes 0 or less, interrupt the process
|
||||
search_preempt_countdown = 100
|
||||
|
||||
materials = context.blend_data.materials
|
||||
|
||||
# Restore the material traversal start position from the last interrupted state.
|
||||
end_material_index = len(materials)
|
||||
start_material_index = self.last_material_index
|
||||
if start_material_index >= end_material_index:
|
||||
self.last_material_index = 0
|
||||
self.last_node_index = 0
|
||||
start_material_index = 0
|
||||
|
||||
# Traverse the materials and enable MToon if necessary.
|
||||
next_material_index = start_material_index
|
||||
for material in materials[start_material_index:end_material_index]:
|
||||
self.last_material_index = next_material_index
|
||||
next_material_index += 1
|
||||
|
||||
search_preempt_countdown -= 1
|
||||
if search_preempt_countdown <= 0:
|
||||
return RunState.PREEMPT
|
||||
|
||||
if bpy.app.version < (5, 0, 0) and not material.use_nodes:
|
||||
continue
|
||||
node_tree = material.node_tree
|
||||
if node_tree is None:
|
||||
continue
|
||||
|
||||
nodes = node_tree.nodes
|
||||
|
||||
# Restore the node traversal start position from the last interrupted state.
|
||||
end_node_index = len(nodes)
|
||||
start_node_index = self.last_node_index
|
||||
if start_node_index >= end_node_index:
|
||||
start_node_index = 0
|
||||
|
||||
# Traverse the nodes and convert the material to MToon if the MToon
|
||||
# placeholder node is connected to ShaderNodeOutputMaterial.
|
||||
next_node_index = start_node_index
|
||||
for node in nodes[start_node_index:end_node_index]:
|
||||
self.last_node_index = next_node_index
|
||||
next_node_index += 1
|
||||
|
||||
search_preempt_countdown -= 1
|
||||
if search_preempt_countdown <= 0:
|
||||
return RunState.PREEMPT
|
||||
|
||||
if not isinstance(node, ShaderNodeGroup):
|
||||
continue
|
||||
|
||||
group_node_tree = node.node_tree
|
||||
if group_node_tree is None:
|
||||
continue
|
||||
|
||||
if not group_node_tree.get(
|
||||
MTOON1_AUTO_SETUP_GROUP_NODE_TREE_CUSTOM_KEY
|
||||
):
|
||||
continue
|
||||
|
||||
found = False
|
||||
for output in node.outputs:
|
||||
for link in output.links:
|
||||
if isinstance(link.to_node, ShaderNodeOutputMaterial):
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if not found:
|
||||
continue
|
||||
|
||||
mtoon1 = get_material_extension(material).mtoon1
|
||||
if mtoon1.enabled:
|
||||
ops.vrm.reset_mtoon1_material_shader_node_group(
|
||||
material_name=material.name
|
||||
)
|
||||
else:
|
||||
mtoon1.enabled = True
|
||||
break
|
||||
self.last_node_index = 0
|
||||
self.last_material_index = 0
|
||||
|
||||
return RunState.FINISH
|
||||
|
||||
def create_fast_path_performance_test_objects(self, context: Context) -> None:
|
||||
blend_data = context.blend_data
|
||||
|
||||
for i in range(100):
|
||||
blend_data.materials.new(f"Material#{i}")
|
||||
|
||||
for i in range(100):
|
||||
mesh = blend_data.meshes.new(f"Mesh#{i}")
|
||||
obj = blend_data.objects.new(f"Object#{i}", mesh)
|
||||
context.scene.collection.objects.link(obj)
|
||||
for k in range(50):
|
||||
material = blend_data.materials[(k * 3) % len(blend_data.materials)]
|
||||
mesh.materials.append(material)
|
||||
if k % 5 == 0:
|
||||
get_material_extension(material).mtoon1.enabled = True
|
||||
Reference in New Issue
Block a user