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:
1
editor/mtoon1/__init__.py
Normal file
1
editor/mtoon1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
editor/mtoon1/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/handler.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/handler.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/migration.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/migration.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/ops.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/ops.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/panel.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/panel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/property_group.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/property_group.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/scene_watcher.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/scene_watcher.cpython-311.pyc
Normal file
Binary file not shown.
24
editor/mtoon1/handler.py
Normal file
24
editor/mtoon1/handler.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from ...common.scene_watcher import trigger_scene_watcher
|
||||
from . import migration
|
||||
from .scene_watcher import MToon1AutoSetup, OutlineUpdater
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@persistent
|
||||
def depsgraph_update_pre(_unused: object) -> None:
|
||||
trigger_scene_watcher(MToon1AutoSetup)
|
||||
if bpy.app.version < (3, 3):
|
||||
return
|
||||
trigger_scene_watcher(OutlineUpdater)
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post(_unsed: object) -> None:
|
||||
migration.state.material_blender_4_2_warning_shown = False
|
||||
529
editor/mtoon1/migration.py
Normal file
529
editor/mtoon1/migration.py
Normal file
@@ -0,0 +1,529 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import functools
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
Context,
|
||||
Image,
|
||||
Material,
|
||||
NodeReroute,
|
||||
PropertyGroup,
|
||||
ShaderNodeGroup,
|
||||
ShaderNodeTexImage,
|
||||
)
|
||||
|
||||
from ...common import convert, ops, shader, version
|
||||
from ...common.gl import GL_LINEAR, GL_NEAREST
|
||||
from ...common.logger import get_logger
|
||||
from ...common.progress import create_progress
|
||||
from .. import search
|
||||
from ..extension import get_material_extension
|
||||
from .property_group import (
|
||||
GL_LINEAR_IMAGE_INTERPOLATIONS,
|
||||
IMAGE_INTERPOLATION_CLOSEST,
|
||||
IMAGE_INTERPOLATION_LINEAR,
|
||||
Mtoon1MaterialPropertyGroup,
|
||||
Mtoon1SamplerPropertyGroup,
|
||||
Mtoon1TextureInfoPropertyGroup,
|
||||
Mtoon1VrmcMaterialsMtoonPropertyGroup,
|
||||
reset_shader_node_group,
|
||||
)
|
||||
|
||||
TextureInfoBackup = Mtoon1TextureInfoPropertyGroup.TextureInfoBackup
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
material_blender_4_2_warning_shown: bool = False
|
||||
|
||||
|
||||
state: Final = State()
|
||||
|
||||
|
||||
def show_material_blender_4_2_warning_delay(material_name_lines: str) -> None:
|
||||
ops.vrm.show_material_blender_4_2_warning(
|
||||
"INVOKE_DEFAULT",
|
||||
material_name_lines=material_name_lines,
|
||||
)
|
||||
|
||||
|
||||
def migrate(context: Context, *, show_progress: bool = False) -> None:
|
||||
blender_4_2_migrated_material_names: list[str] = []
|
||||
|
||||
with create_progress(context, show_progress=show_progress) as progress:
|
||||
for material_index, material in enumerate(context.blend_data.materials):
|
||||
if not material:
|
||||
continue
|
||||
migrate_material(context, material, blender_4_2_migrated_material_names)
|
||||
progress.update(float(material_index) / len(context.blend_data.materials))
|
||||
progress.update(1)
|
||||
|
||||
if (
|
||||
blender_4_2_migrated_material_names
|
||||
and tuple(context.blend_data.version) < (4, 2)
|
||||
and bpy.app.version >= (4, 2)
|
||||
):
|
||||
logger.warning(
|
||||
"Migrating Materials from blender version data=%s app=%s",
|
||||
context.blend_data.version,
|
||||
bpy.app.version,
|
||||
)
|
||||
|
||||
if not state.material_blender_4_2_warning_shown:
|
||||
state.material_blender_4_2_warning_shown = True
|
||||
# In Blender 4.2.0, if you don't run it with a timer, the dialog will
|
||||
# disappear automatically.
|
||||
bpy.app.timers.register(
|
||||
functools.partial(
|
||||
show_material_blender_4_2_warning_delay,
|
||||
"\n".join(blender_4_2_migrated_material_names),
|
||||
),
|
||||
first_interval=0.1,
|
||||
)
|
||||
|
||||
|
||||
def migrate_material(
|
||||
context: Context,
|
||||
material: Material,
|
||||
blender_4_2_migrated_material_names: list[str],
|
||||
) -> None:
|
||||
_, legacy_legacy_shader_name = search.legacy_shader_node(material)
|
||||
if legacy_legacy_shader_name in search.LEGACY_SHADER_NAMES:
|
||||
# Since old shader node groups are not compatible with Blender 4.2 as they are,
|
||||
# always warn when upgrading to Blender 4.2 or later.
|
||||
blender_4_2_migrated_material_names.append(material.name)
|
||||
return
|
||||
|
||||
if bpy.app.version < (5, 0, 0) and not material.use_nodes:
|
||||
return
|
||||
node_tree = material.node_tree
|
||||
if not node_tree:
|
||||
return
|
||||
|
||||
mtoon1 = get_material_extension(material).mtoon1
|
||||
if not mtoon1.get("enabled"):
|
||||
return
|
||||
|
||||
vrmc_materials_mtoon = mtoon1.extensions.vrmc_materials_mtoon
|
||||
if vrmc_materials_mtoon.get("is_outline_material"):
|
||||
return
|
||||
|
||||
addon_version = convert.float3_or(mtoon1.get("addon_version"), (0, 0, 0))
|
||||
if addon_version < (2, 16, 4):
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_10_0/io_scene_vrm/editor/mtoon1/property_group.py#L1658-L1683
|
||||
surface_node_name = "Mtoon1Material.MaterialOutputSurfaceIn"
|
||||
surface_node = node_tree.nodes.get(surface_node_name)
|
||||
if not isinstance(surface_node, NodeReroute):
|
||||
return
|
||||
|
||||
connected = False
|
||||
surface_socket = surface_node.outputs[0]
|
||||
for link in node_tree.links:
|
||||
if (
|
||||
link.from_socket == surface_socket
|
||||
and link.to_socket
|
||||
and link.to_socket.node
|
||||
and link.to_socket.node.type == "OUTPUT_MATERIAL"
|
||||
):
|
||||
connected = True
|
||||
break
|
||||
if not connected:
|
||||
return
|
||||
else:
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_16_4/io_scene_vrm/editor/mtoon1/property_group.py#L1913-L1929
|
||||
group_node = node_tree.nodes.get("Mtoon1Material.Mtoon1Output")
|
||||
if not isinstance(group_node, ShaderNodeGroup):
|
||||
return
|
||||
if not group_node.node_tree:
|
||||
return
|
||||
if group_node.node_tree.name != "VRM Add-on MToon 1.0 Output Revision 1":
|
||||
return
|
||||
|
||||
if addon_version < (2, 20, 50):
|
||||
migrate_sampler_filter_node(material)
|
||||
|
||||
alpha_mode: Optional[str] = None
|
||||
alpha_cutoff: Optional[float] = None
|
||||
if addon_version < (2, 20, 55):
|
||||
blender_4_2_migrated_material_names.append(material.name)
|
||||
alpha_cutoff = material.alpha_threshold
|
||||
blend_method = material.blend_method
|
||||
if blend_method in ["BLEND", "HASHED"]:
|
||||
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_BLEND.identifier
|
||||
elif blend_method == "CLIP":
|
||||
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_MASK.identifier
|
||||
else:
|
||||
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_OPAQUE.identifier
|
||||
|
||||
base_color_factor: Optional[tuple[float, float, float, float]] = None
|
||||
base_color_texture_backup: Optional[TextureInfoBackup] = None
|
||||
normal_texture_backup: Optional[TextureInfoBackup] = None
|
||||
normal_texture_scale: Optional[float] = None
|
||||
emissive_texture_backup: Optional[TextureInfoBackup] = None
|
||||
emissive_factor: Optional[tuple[float, float, float]] = None
|
||||
emissive_strength: Optional[float] = None
|
||||
|
||||
transparent_with_z_write: Optional[bool] = None
|
||||
render_queue_offset_number: Optional[int] = None
|
||||
shade_multiply_texture_backup: Optional[TextureInfoBackup] = None
|
||||
shade_color_factor: Optional[tuple[float, float, float]] = None
|
||||
shading_shift_texture_backup: Optional[TextureInfoBackup] = None
|
||||
shading_shift_texture_scale: Optional[float] = None
|
||||
shading_shift_factor: Optional[float] = None
|
||||
shading_toony_factor: Optional[float] = None
|
||||
gi_equalization_factor: Optional[float] = None
|
||||
matcap_factor: Optional[tuple[float, float, float]] = None
|
||||
matcap_texture_backup: Optional[TextureInfoBackup] = None
|
||||
parametric_rim_color_factor: Optional[tuple[float, float, float]] = None
|
||||
rim_multiply_texture_backup: Optional[TextureInfoBackup] = None
|
||||
rim_lighting_mix_factor: Optional[float] = None
|
||||
parametric_rim_fresnel_power_factor: Optional[float] = None
|
||||
parametric_rim_lift_factor: Optional[float] = None
|
||||
outline_width_mode: Optional[str] = None
|
||||
outline_width_factor: Optional[float] = None
|
||||
outline_width_multiply_texture_backup: Optional[TextureInfoBackup] = None
|
||||
outline_color_factor: Optional[tuple[float, float, float]] = None
|
||||
outline_lighting_mix_factor: Optional[float] = None
|
||||
uv_animation_mask_texture_backup: Optional[TextureInfoBackup] = None
|
||||
uv_animation_scroll_x_speed_factor: Optional[float] = None
|
||||
uv_animation_scroll_y_speed_factor: Optional[float] = None
|
||||
uv_animation_rotation_speed_factor: Optional[float] = None
|
||||
if addon_version < (2, 20, 62):
|
||||
pbr_metallic_roughness = mtoon1.pbr_metallic_roughness
|
||||
base_color_factor = convert.float4_or_none(
|
||||
pbr_metallic_roughness.get("base_color_factor")
|
||||
)
|
||||
base_color_texture_backup = backup_texture_info(
|
||||
pbr_metallic_roughness.base_color_texture
|
||||
)
|
||||
normal_texture = mtoon1.normal_texture
|
||||
normal_texture_backup = backup_texture_info(normal_texture)
|
||||
normal_texture_scale = convert.float_or_none(normal_texture.get("scale"))
|
||||
emissive_factor = convert.float3_or_none(mtoon1.get("emissive_factor"))
|
||||
emissive_texture_backup = backup_texture_info(mtoon1.emissive_texture)
|
||||
emissive_strength = convert.float_or_none(
|
||||
mtoon1.extensions.khr_materials_emissive_strength.get("emissive_strength")
|
||||
)
|
||||
|
||||
transparent_with_z_write_object = vrmc_materials_mtoon.get(
|
||||
"transparent_with_z_write"
|
||||
)
|
||||
if isinstance(transparent_with_z_write_object, int):
|
||||
transparent_with_z_write = bool(transparent_with_z_write_object)
|
||||
|
||||
render_queue_offset_number_object = vrmc_materials_mtoon.get(
|
||||
"render_queue_offset_number"
|
||||
)
|
||||
if isinstance(render_queue_offset_number_object, int):
|
||||
render_queue_offset_number = render_queue_offset_number_object
|
||||
|
||||
shade_multiply_texture_backup = backup_texture_info(
|
||||
vrmc_materials_mtoon.shade_multiply_texture
|
||||
)
|
||||
shade_color_factor = convert.float3_or_none(
|
||||
vrmc_materials_mtoon.get("shade_color_factor")
|
||||
)
|
||||
|
||||
shading_shift_texture = vrmc_materials_mtoon.shading_shift_texture
|
||||
shading_shift_texture_backup = backup_texture_info(shading_shift_texture)
|
||||
shading_shift_texture_scale = convert.float_or_none(
|
||||
shading_shift_texture.get("scale")
|
||||
)
|
||||
|
||||
shading_shift_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("shading_shift_factor")
|
||||
)
|
||||
shading_toony_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("shading_toony_factor")
|
||||
)
|
||||
matcap_factor = convert.float3_or_none(
|
||||
vrmc_materials_mtoon.get("matcap_factor")
|
||||
)
|
||||
matcap_texture_backup = backup_texture_info(vrmc_materials_mtoon.matcap_texture)
|
||||
gi_equalization_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("gi_equalization_factor")
|
||||
)
|
||||
parametric_rim_color_factor = convert.float3_or_none(
|
||||
vrmc_materials_mtoon.get("parametric_rim_color_factor")
|
||||
)
|
||||
rim_multiply_texture_backup = backup_texture_info(
|
||||
vrmc_materials_mtoon.rim_multiply_texture
|
||||
)
|
||||
rim_lighting_mix_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("rim_lighting_mix_factor")
|
||||
)
|
||||
parametric_rim_fresnel_power_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("parametric_rim_fresnel_power_factor")
|
||||
)
|
||||
parametric_rim_lift_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("parametric_rim_lift_factor")
|
||||
)
|
||||
outline_width_mode_number = vrmc_materials_mtoon.get("outline_width_mode")
|
||||
if isinstance(outline_width_mode_number, int):
|
||||
outline_width_mode_item = (
|
||||
Mtoon1VrmcMaterialsMtoonPropertyGroup.outline_width_mode_enum
|
||||
)
|
||||
outline_width_mode = outline_width_mode_item.value_to_identifier(
|
||||
outline_width_mode_number,
|
||||
Mtoon1VrmcMaterialsMtoonPropertyGroup.OUTLINE_WIDTH_MODE_NONE.identifier,
|
||||
)
|
||||
outline_width_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("outline_width_factor")
|
||||
)
|
||||
outline_width_multiply_texture_backup = backup_texture_info(
|
||||
vrmc_materials_mtoon.outline_width_multiply_texture
|
||||
)
|
||||
outline_color_factor = convert.float3_or_none(
|
||||
vrmc_materials_mtoon.get("outline_color_factor")
|
||||
)
|
||||
outline_lighting_mix_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("outline_lighting_mix_factor")
|
||||
)
|
||||
uv_animation_mask_texture_backup = backup_texture_info(
|
||||
vrmc_materials_mtoon.uv_animation_mask_texture
|
||||
)
|
||||
uv_animation_scroll_x_speed_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("uv_animation_scroll_x_speed_factor")
|
||||
)
|
||||
uv_animation_scroll_y_speed_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("uv_animation_scroll_y_speed_factor")
|
||||
)
|
||||
uv_animation_rotation_speed_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("uv_animation_rotation_speed_factor")
|
||||
)
|
||||
|
||||
if (
|
||||
addon_version < shader.LAST_MODIFIED_VERSION
|
||||
# Blender 4.2 has a node specification change, so it will be forcibly reset.
|
||||
or (bpy.app.version >= (4, 2) and tuple(context.blend_data.version) < (4, 2))
|
||||
):
|
||||
reset_shader_node_group(
|
||||
context, material, reset_material_node_tree=True, reset_node_groups=False
|
||||
)
|
||||
|
||||
# From this point on, you can write code that assumes the shader node is up to date.
|
||||
vrmc_materials_mtoon = mtoon1.extensions.vrmc_materials_mtoon
|
||||
if alpha_mode is not None:
|
||||
mtoon1.alpha_mode = alpha_mode
|
||||
if alpha_cutoff is not None:
|
||||
mtoon1.alpha_cutoff = alpha_cutoff
|
||||
if base_color_factor is not None:
|
||||
mtoon1.pbr_metallic_roughness.base_color_factor = base_color_factor
|
||||
if base_color_texture_backup is not None:
|
||||
mtoon1.pbr_metallic_roughness.base_color_texture.restore(
|
||||
base_color_texture_backup
|
||||
)
|
||||
if normal_texture_backup is not None:
|
||||
mtoon1.normal_texture.restore(normal_texture_backup)
|
||||
if normal_texture_scale is not None:
|
||||
mtoon1.normal_texture.scale = normal_texture_scale
|
||||
if emissive_texture_backup is not None:
|
||||
mtoon1.emissive_texture.restore(emissive_texture_backup)
|
||||
if emissive_factor is not None:
|
||||
mtoon1.emissive_factor = emissive_factor
|
||||
if emissive_strength is not None:
|
||||
mtoon1.extensions.khr_materials_emissive_strength.emissive_strength = (
|
||||
emissive_strength
|
||||
)
|
||||
|
||||
if transparent_with_z_write is not None:
|
||||
vrmc_materials_mtoon.transparent_with_z_write = transparent_with_z_write
|
||||
if render_queue_offset_number is not None:
|
||||
vrmc_materials_mtoon.render_queue_offset_number = render_queue_offset_number
|
||||
if shade_multiply_texture_backup is not None:
|
||||
vrmc_materials_mtoon.shade_multiply_texture.restore(
|
||||
shade_multiply_texture_backup
|
||||
)
|
||||
if shade_color_factor is not None:
|
||||
vrmc_materials_mtoon.shade_color_factor = shade_color_factor
|
||||
if shading_shift_texture_backup is not None:
|
||||
vrmc_materials_mtoon.shading_shift_texture.restore(shading_shift_texture_backup)
|
||||
if shading_shift_factor is not None:
|
||||
vrmc_materials_mtoon.shading_shift_factor = shading_shift_factor
|
||||
if shading_shift_texture_scale is not None:
|
||||
vrmc_materials_mtoon.shading_shift_texture.scale = shading_shift_texture_scale
|
||||
if shading_toony_factor is not None:
|
||||
vrmc_materials_mtoon.shading_toony_factor = shading_toony_factor
|
||||
if gi_equalization_factor is not None:
|
||||
vrmc_materials_mtoon.gi_equalization_factor = gi_equalization_factor
|
||||
if matcap_factor is not None:
|
||||
vrmc_materials_mtoon.matcap_factor = matcap_factor
|
||||
if matcap_texture_backup is not None:
|
||||
vrmc_materials_mtoon.matcap_texture.restore(matcap_texture_backup)
|
||||
if parametric_rim_color_factor is not None:
|
||||
vrmc_materials_mtoon.parametric_rim_color_factor = parametric_rim_color_factor
|
||||
if rim_multiply_texture_backup is not None:
|
||||
vrmc_materials_mtoon.rim_multiply_texture.restore(rim_multiply_texture_backup)
|
||||
if rim_lighting_mix_factor is not None:
|
||||
vrmc_materials_mtoon.rim_lighting_mix_factor = rim_lighting_mix_factor
|
||||
if parametric_rim_fresnel_power_factor is not None:
|
||||
vrmc_materials_mtoon.parametric_rim_fresnel_power_factor = (
|
||||
parametric_rim_fresnel_power_factor
|
||||
)
|
||||
if parametric_rim_lift_factor is not None:
|
||||
vrmc_materials_mtoon.parametric_rim_lift_factor = parametric_rim_lift_factor
|
||||
if outline_width_mode is not None:
|
||||
vrmc_materials_mtoon.outline_width_mode = outline_width_mode
|
||||
if outline_width_factor is not None:
|
||||
vrmc_materials_mtoon.outline_width_factor = outline_width_factor
|
||||
if outline_width_multiply_texture_backup is not None:
|
||||
vrmc_materials_mtoon.outline_width_multiply_texture.restore(
|
||||
outline_width_multiply_texture_backup
|
||||
)
|
||||
if outline_color_factor is not None:
|
||||
vrmc_materials_mtoon.outline_color_factor = outline_color_factor
|
||||
if outline_lighting_mix_factor is not None:
|
||||
vrmc_materials_mtoon.outline_lighting_mix_factor = outline_lighting_mix_factor
|
||||
if uv_animation_mask_texture_backup is not None:
|
||||
vrmc_materials_mtoon.uv_animation_mask_texture.restore(
|
||||
uv_animation_mask_texture_backup
|
||||
)
|
||||
if uv_animation_scroll_x_speed_factor is not None:
|
||||
vrmc_materials_mtoon.uv_animation_scroll_x_speed_factor = (
|
||||
uv_animation_scroll_x_speed_factor
|
||||
)
|
||||
if uv_animation_scroll_y_speed_factor is not None:
|
||||
vrmc_materials_mtoon.uv_animation_scroll_y_speed_factor = (
|
||||
uv_animation_scroll_y_speed_factor
|
||||
)
|
||||
if uv_animation_rotation_speed_factor is not None:
|
||||
vrmc_materials_mtoon.uv_animation_rotation_speed_factor = (
|
||||
uv_animation_rotation_speed_factor
|
||||
)
|
||||
|
||||
mtoon1.setup_drivers()
|
||||
updated_addon_version = version.get_addon_version()
|
||||
if tuple(mtoon1.addon_version) != updated_addon_version:
|
||||
mtoon1.addon_version = updated_addon_version
|
||||
|
||||
|
||||
def backup_texture_info(texture_info: object) -> Optional[TextureInfoBackup]:
|
||||
if not isinstance(texture_info, PropertyGroup):
|
||||
return None
|
||||
|
||||
source: Optional[Image] = None
|
||||
wrap_s = Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
|
||||
wrap_t = Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
|
||||
offset_x = 0.0
|
||||
offset_y = 0.0
|
||||
scale_x = 1.0
|
||||
scale_y = 1.0
|
||||
|
||||
index = getattr(texture_info, "index", None)
|
||||
if isinstance(index, PropertyGroup):
|
||||
source_object = index.get("source")
|
||||
if isinstance(source_object, Image):
|
||||
source = source_object
|
||||
sampler = getattr(index, "sampler", None)
|
||||
if isinstance(sampler, PropertyGroup):
|
||||
wrap_s_number = sampler.get("wrap_s")
|
||||
if isinstance(wrap_s_number, int):
|
||||
wrap_s = Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
|
||||
wrap_s_number,
|
||||
wrap_s,
|
||||
)
|
||||
wrap_t_number = sampler.get("wrap_t")
|
||||
if isinstance(wrap_t_number, int):
|
||||
wrap_t = Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
|
||||
wrap_t_number,
|
||||
wrap_t,
|
||||
)
|
||||
|
||||
extensions = getattr(texture_info, "extensions", None)
|
||||
if isinstance(extensions, PropertyGroup):
|
||||
khr_texture_transform = getattr(extensions, "khr_texture_transform", None)
|
||||
if isinstance(khr_texture_transform, PropertyGroup):
|
||||
offset = convert.float2_or_none(khr_texture_transform.get("offset"))
|
||||
if offset is not None:
|
||||
offset_x, offset_y = offset
|
||||
scale = convert.float2_or_none(khr_texture_transform.get("scale"))
|
||||
if scale is not None:
|
||||
scale_x, scale_y = scale
|
||||
|
||||
return TextureInfoBackup(
|
||||
source=source,
|
||||
mag_filter=None,
|
||||
min_filter=None,
|
||||
wrap_s=wrap_s,
|
||||
wrap_t=wrap_t,
|
||||
offset=(offset_x, offset_y),
|
||||
scale=(scale_x, scale_y),
|
||||
)
|
||||
|
||||
|
||||
def migrate_sampler_filter_node(material: Material) -> None:
|
||||
node_tree = material.node_tree
|
||||
if not node_tree:
|
||||
return
|
||||
|
||||
mtoon1 = get_material_extension(material).mtoon1
|
||||
|
||||
for node_name, attrs in [
|
||||
(
|
||||
"Mtoon1BaseColorTexture.Image",
|
||||
("pbr_metallic_roughness", "base_color_texture"),
|
||||
),
|
||||
("Mtoon1EmissiveTexture.Image", ("emissive_texture",)),
|
||||
("Mtoon1NormalTexture.Image", ("normal_texture",)),
|
||||
(
|
||||
"Mtoon1ShadeMultiplyTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "shade_multiply_texture"),
|
||||
),
|
||||
(
|
||||
"Mtoon1ShadingShiftTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "shading_shift_texture"),
|
||||
),
|
||||
(
|
||||
"Mtoon1OutlineWidthMultiplyTexture.Image",
|
||||
(
|
||||
"extensions",
|
||||
"vrmc_materials_mtoon",
|
||||
"outline_width_multiply_texture",
|
||||
),
|
||||
),
|
||||
(
|
||||
"Mtoon1UvAnimationMaskTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "uv_animation_mask_texture"),
|
||||
),
|
||||
(
|
||||
"Mtoon1MatcapTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "matcap_texture"),
|
||||
),
|
||||
(
|
||||
"Mtoon1RimMultiplyTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "rim_multiply_texture"),
|
||||
),
|
||||
]:
|
||||
sampler = functools.reduce(
|
||||
lambda prop, attr: getattr(prop, attr, None),
|
||||
(*attrs, "index", "sampler"),
|
||||
mtoon1,
|
||||
)
|
||||
if not isinstance(sampler, PropertyGroup):
|
||||
continue
|
||||
|
||||
mag_filter = sampler.get("mag_filter")
|
||||
if (
|
||||
not isinstance(mag_filter, int)
|
||||
or mag_filter not in Mtoon1SamplerPropertyGroup.mag_filter_enum.values()
|
||||
):
|
||||
continue
|
||||
|
||||
node = node_tree.nodes.get(node_name)
|
||||
if not isinstance(node, ShaderNodeTexImage):
|
||||
continue
|
||||
|
||||
if (
|
||||
mag_filter == GL_NEAREST
|
||||
and node.interpolation != IMAGE_INTERPOLATION_CLOSEST
|
||||
):
|
||||
node.interpolation = IMAGE_INTERPOLATION_CLOSEST
|
||||
if (
|
||||
mag_filter == GL_LINEAR
|
||||
and node.interpolation not in GL_LINEAR_IMAGE_INTERPOLATIONS
|
||||
):
|
||||
node.interpolation = IMAGE_INTERPOLATION_LINEAR
|
||||
1158
editor/mtoon1/ops.py
Normal file
1158
editor/mtoon1/ops.py
Normal file
File diff suppressed because it is too large
Load Diff
447
editor/mtoon1/panel.py
Normal file
447
editor/mtoon1/panel.py
Normal file
@@ -0,0 +1,447 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from typing import Optional
|
||||
|
||||
import bpy
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.types import Context, Panel, PropertyGroup, UILayout
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from .. import search
|
||||
from ..extension import get_material_extension
|
||||
from ..ops import VRM_OT_open_url_in_web_browser, layout_operator
|
||||
from .ops import (
|
||||
VRM_OT_import_mtoon1_texture_image_file,
|
||||
VRM_OT_reset_mtoon1_material_shader_node_tree,
|
||||
)
|
||||
from .property_group import (
|
||||
Mtoon0TexturePropertyGroup,
|
||||
Mtoon1MaterialPropertyGroup,
|
||||
Mtoon1TextureInfoPropertyGroup,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def draw_texture_info(
|
||||
material_name: str,
|
||||
ext: Mtoon1MaterialPropertyGroup,
|
||||
parent_layout: UILayout,
|
||||
base_property_group: PropertyGroup,
|
||||
texture_info_attr_name: str,
|
||||
color_factor_attr_name: Optional[str] = None,
|
||||
*,
|
||||
is_vrm0: bool,
|
||||
) -> UILayout:
|
||||
texture_info = getattr(base_property_group, texture_info_attr_name, None)
|
||||
if not isinstance(texture_info, Mtoon1TextureInfoPropertyGroup):
|
||||
raise TypeError
|
||||
layout = parent_layout.split(factor=0.3)
|
||||
toggle_layout = layout.row()
|
||||
toggle_layout.alignment = "LEFT"
|
||||
toggle_layout.prop(
|
||||
texture_info,
|
||||
"show_expanded",
|
||||
emboss=False,
|
||||
text=texture_info.index.panel_label,
|
||||
translate=False,
|
||||
icon="TRIA_DOWN" if texture_info.show_expanded else "TRIA_RIGHT",
|
||||
)
|
||||
input_layout = layout.row(align=True)
|
||||
|
||||
node_image = texture_info.index.get_connected_node_image()
|
||||
if node_image == texture_info.index.source:
|
||||
source_prop_name = "source"
|
||||
placeholder = ""
|
||||
else:
|
||||
source_prop_name = "source_for_desynced_node_tree"
|
||||
placeholder = node_image.name if node_image else ""
|
||||
|
||||
if bpy.app.version >= (4, 1):
|
||||
input_layout.prop(
|
||||
texture_info.index,
|
||||
source_prop_name,
|
||||
text="",
|
||||
translate=False,
|
||||
placeholder=placeholder,
|
||||
)
|
||||
else:
|
||||
input_layout.prop(
|
||||
texture_info.index,
|
||||
source_prop_name,
|
||||
text="",
|
||||
translate=False,
|
||||
)
|
||||
|
||||
import_image_file_op = layout_operator(
|
||||
input_layout,
|
||||
VRM_OT_import_mtoon1_texture_image_file,
|
||||
text="",
|
||||
translate=False,
|
||||
icon="FILEBROWSER",
|
||||
)
|
||||
import_image_file_op.material_name = material_name
|
||||
import_image_file_op.target_texture = type(texture_info.index).__name__
|
||||
if color_factor_attr_name:
|
||||
input_layout.separator(factor=0.5)
|
||||
input_layout.prop(base_property_group, color_factor_attr_name, text="")
|
||||
|
||||
if not texture_info.show_expanded:
|
||||
return input_layout
|
||||
|
||||
box = parent_layout.box().column()
|
||||
if texture_info.index.source:
|
||||
box.prop(texture_info.index.source.colorspace_settings, "name")
|
||||
if (
|
||||
texture_info.index.source.colorspace_settings.name
|
||||
!= texture_info.index.colorspace
|
||||
):
|
||||
box.box().label(
|
||||
text=pgettext(
|
||||
'It is recommended to set "{colorspace}"'
|
||||
+ ' to "{input_colorspace}" for "{texture_label}"'
|
||||
).format(
|
||||
texture_label=texture_info.index.label,
|
||||
colorspace=pgettext(texture_info.index.colorspace),
|
||||
input_colorspace=pgettext("Input Color Space"),
|
||||
),
|
||||
icon="ERROR",
|
||||
)
|
||||
box.prop(texture_info.index.sampler, "mag_filter")
|
||||
box.prop(texture_info.index.sampler, "min_filter")
|
||||
box.prop(texture_info.index.sampler, "wrap_s")
|
||||
box.prop(texture_info.index.sampler, "wrap_t")
|
||||
box.prop(texture_info.extensions.khr_texture_transform, "offset")
|
||||
box.prop(texture_info.extensions.khr_texture_transform, "scale")
|
||||
|
||||
if is_vrm0:
|
||||
if ext.extensions.vrmc_materials_mtoon.matcap_texture == texture_info:
|
||||
box.box().label(
|
||||
text="Offset and Scale are ignored in VRM 0.0", icon="ERROR"
|
||||
)
|
||||
elif ext.pbr_metallic_roughness.base_color_texture != texture_info:
|
||||
box.box().label(
|
||||
text="Offset and Scale in VRM 0.0 are"
|
||||
+ " the values of the Lit Color Texture",
|
||||
icon="ERROR",
|
||||
)
|
||||
|
||||
return input_layout
|
||||
|
||||
|
||||
def draw_mtoon0_texture(
|
||||
material_name: str,
|
||||
parent_layout: UILayout,
|
||||
base_property_group: PropertyGroup,
|
||||
texture_attr_name: str,
|
||||
scalar_factor_attr_name: str,
|
||||
) -> UILayout:
|
||||
texture = getattr(base_property_group, texture_attr_name, None)
|
||||
if not isinstance(texture, Mtoon0TexturePropertyGroup):
|
||||
raise TypeError
|
||||
layout = parent_layout.split(factor=0.3)
|
||||
toggle_layout = layout.row()
|
||||
toggle_layout.alignment = "LEFT"
|
||||
toggle_layout.prop(
|
||||
texture,
|
||||
"show_expanded",
|
||||
emboss=False,
|
||||
text=texture.panel_label,
|
||||
translate=False,
|
||||
icon="TRIA_DOWN" if texture.show_expanded else "TRIA_RIGHT",
|
||||
)
|
||||
input_layout = layout.row(align=True)
|
||||
input_layout.prop(texture, "source", text="")
|
||||
import_image_file_op = layout_operator(
|
||||
input_layout,
|
||||
VRM_OT_import_mtoon1_texture_image_file,
|
||||
text="",
|
||||
translate=False,
|
||||
icon="FILEBROWSER",
|
||||
)
|
||||
import_image_file_op.material_name = material_name
|
||||
import_image_file_op.target_texture = type(texture).__name__
|
||||
|
||||
input_layout.separator(factor=0.5)
|
||||
input_layout.prop(base_property_group, scalar_factor_attr_name, text="")
|
||||
|
||||
if not texture.show_expanded:
|
||||
return input_layout
|
||||
|
||||
box = parent_layout.box().column()
|
||||
if texture.source:
|
||||
box.prop(texture.source.colorspace_settings, "name")
|
||||
if texture.source.colorspace_settings.name != texture.colorspace:
|
||||
box.box().label(
|
||||
text=pgettext(
|
||||
'It is recommended to set "{colorspace}"'
|
||||
+ ' to "{input_colorspace}" for "{texture_label}"'
|
||||
).format(
|
||||
texture_label=texture.label,
|
||||
colorspace=pgettext(texture.colorspace),
|
||||
input_colorspace=pgettext("Input Color Space"),
|
||||
),
|
||||
icon="ERROR",
|
||||
)
|
||||
box.prop(texture.sampler, "mag_filter")
|
||||
box.prop(texture.sampler, "min_filter")
|
||||
box.prop(texture.sampler, "wrap_s")
|
||||
box.prop(texture.sampler, "wrap_t")
|
||||
return input_layout
|
||||
|
||||
|
||||
def draw_mtoon1_material(context: Context, layout: UILayout) -> None:
|
||||
material = context.material
|
||||
if not material:
|
||||
return
|
||||
ext = get_material_extension(material)
|
||||
layout = layout.column()
|
||||
|
||||
layout.prop(ext.mtoon1, "enabled")
|
||||
if not ext.mtoon1.enabled:
|
||||
return
|
||||
|
||||
is_vrm0 = search.current_armature_is_vrm0(context)
|
||||
gltf = ext.mtoon1
|
||||
mtoon = gltf.extensions.vrmc_materials_mtoon
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/v0.102.0/Assets/VRMShaders/VRM10/MToon10/Editor/MToonInspector.cs#L14
|
||||
layout.label(text="Rendering", translate=False)
|
||||
rendering_box = layout.box().column()
|
||||
rendering_box.prop(gltf, "alpha_mode")
|
||||
if gltf.alpha_mode == gltf.ALPHA_MODE_MASK.identifier:
|
||||
rendering_box.prop(gltf, "alpha_cutoff", slider=True)
|
||||
if gltf.alpha_mode == gltf.ALPHA_MODE_BLEND.identifier:
|
||||
rendering_box.prop(mtoon, "transparent_with_z_write")
|
||||
rendering_box.prop(gltf, "double_sided")
|
||||
rendering_box.prop(mtoon, "render_queue_offset_number", slider=True)
|
||||
|
||||
layout.label(text="Lighting", translate=False)
|
||||
lighting_box = layout.box().column()
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
lighting_box,
|
||||
gltf.pbr_metallic_roughness,
|
||||
"base_color_texture",
|
||||
"base_color_factor",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
lighting_box,
|
||||
mtoon,
|
||||
"shade_multiply_texture",
|
||||
"shade_color_factor",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
normal_texture_layout = draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
lighting_box,
|
||||
gltf,
|
||||
"normal_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
normal_texture_layout.separator(factor=0.5)
|
||||
normal_texture_layout.prop(gltf.normal_texture, "scale")
|
||||
|
||||
lighting_box.prop(mtoon, "shading_toony_factor", slider=True)
|
||||
lighting_box.prop(mtoon, "shading_shift_factor", slider=True)
|
||||
shading_shift_texture_layout = draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
lighting_box,
|
||||
mtoon,
|
||||
"shading_shift_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
shading_shift_texture_layout.separator(factor=0.5)
|
||||
shading_shift_texture_layout.prop(mtoon.shading_shift_texture, "scale")
|
||||
|
||||
# UniVRM (MIT License)
|
||||
# https://github.com/vrm-c/UniVRM/blob/d2b4ad1964b754341873f2a1b093d58e1df1713f/Assets/VRMShaders/VRM10/MToon10/Editor/MToonInspector.cs#L120-L128
|
||||
if (
|
||||
not mtoon.shading_shift_texture.index.source
|
||||
and mtoon.shading_toony_factor - mtoon.shading_shift_factor < 1.0 - 0.001
|
||||
):
|
||||
lighting_box.box().label(
|
||||
text="The lit area includes non-lit area.", icon="ERROR"
|
||||
)
|
||||
|
||||
layout.label(text="Global Illumination", translate=False)
|
||||
gi_box = layout.box()
|
||||
gi_box.prop(mtoon, "gi_equalization_factor", slider=True)
|
||||
|
||||
layout.label(text="Emission", translate=False)
|
||||
emission_box = layout.box().column()
|
||||
emissive_texture_layout = draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
emission_box,
|
||||
gltf,
|
||||
"emissive_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
).row(align=True)
|
||||
emissive_texture_layout.scale_x = 0.71
|
||||
emissive_texture_layout.separator(factor=0.5 / 0.71)
|
||||
emissive_texture_layout.prop(gltf, "emissive_factor", text="")
|
||||
emissive_texture_layout.prop(
|
||||
gltf.extensions.khr_materials_emissive_strength, "emissive_strength"
|
||||
)
|
||||
|
||||
layout.label(text="Rim Lighting", translate=False)
|
||||
rim_lighting_box = layout.box().column()
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
rim_lighting_box,
|
||||
mtoon,
|
||||
"rim_multiply_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
rim_lighting_box.prop(mtoon, "rim_lighting_mix_factor", slider=True)
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
rim_lighting_box,
|
||||
mtoon,
|
||||
"matcap_texture",
|
||||
"matcap_factor",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
rim_lighting_box.row().prop(mtoon, "parametric_rim_color_factor")
|
||||
rim_lighting_box.prop(mtoon, "parametric_rim_fresnel_power_factor", slider=True)
|
||||
rim_lighting_box.prop(mtoon, "parametric_rim_lift_factor", slider=True)
|
||||
|
||||
layout.label(text="Outline", translate=False)
|
||||
outline_box = layout.box().column()
|
||||
outline_box.prop(mtoon, "outline_width_mode")
|
||||
if (
|
||||
bpy.app.version >= (3, 3)
|
||||
and mtoon.outline_width_mode
|
||||
== mtoon.OUTLINE_WIDTH_MODE_SCREEN_COORDINATES.identifier
|
||||
):
|
||||
outline_warning_message = pgettext(
|
||||
'The "Screen Coordinates" display is not yet implemented.\n'
|
||||
+ 'It is displayed in the same way as "World Coordinates".'
|
||||
)
|
||||
outline_warning_column = outline_box.box().column(align=True)
|
||||
for index, outline_warning_line in enumerate(
|
||||
outline_warning_message.splitlines()
|
||||
):
|
||||
outline_warning_column.label(
|
||||
text=outline_warning_line,
|
||||
translate=False,
|
||||
icon="BLANK1" if index else "INFO",
|
||||
)
|
||||
if mtoon.outline_width_mode != mtoon.OUTLINE_WIDTH_MODE_NONE.identifier:
|
||||
outline_width_multiply_texture_layout = draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
outline_box,
|
||||
mtoon,
|
||||
"outline_width_multiply_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
outline_width_multiply_texture_layout.separator(factor=0.5)
|
||||
outline_width_multiply_texture_layout.prop(
|
||||
mtoon, "outline_width_factor", slider=True, text=""
|
||||
)
|
||||
outline_box.row().prop(mtoon, "outline_color_factor")
|
||||
outline_box.prop(mtoon, "outline_lighting_mix_factor", slider=True)
|
||||
outline_box.prop(mtoon, "enable_outline_preview", text="Enable Preview")
|
||||
|
||||
layout.label(text="UV Animation", translate=False)
|
||||
uv_animation_box = layout.box().column()
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
uv_animation_box,
|
||||
mtoon,
|
||||
"uv_animation_mask_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
uv_animation_box.prop(mtoon, "uv_animation_scroll_x_speed_factor")
|
||||
uv_animation_box.prop(mtoon, "uv_animation_scroll_y_speed_factor")
|
||||
uv_animation_box.prop(mtoon, "uv_animation_rotation_speed_factor")
|
||||
|
||||
layout.prop(gltf, "show_expanded_mtoon0")
|
||||
if gltf.show_expanded_mtoon0:
|
||||
mtoon0_box = layout.box().column()
|
||||
mtoon0_box.prop(gltf, "mtoon0_front_cull_mode")
|
||||
draw_mtoon0_texture(
|
||||
material.name,
|
||||
mtoon0_box,
|
||||
ext.mtoon1,
|
||||
"mtoon0_receive_shadow_texture",
|
||||
"mtoon0_receive_shadow_rate",
|
||||
)
|
||||
draw_mtoon0_texture(
|
||||
material.name,
|
||||
mtoon0_box,
|
||||
ext.mtoon1,
|
||||
"mtoon0_shading_grade_texture",
|
||||
"mtoon0_shading_grade_rate",
|
||||
)
|
||||
mtoon0_box.prop(gltf, "mtoon0_light_color_attenuation", slider=True)
|
||||
mtoon0_box.prop(gltf, "mtoon0_rim_lighting_mix", slider=True)
|
||||
mtoon0_box.prop(gltf, "mtoon0_outline_scaled_max_distance", slider=True)
|
||||
mtoon0_box.prop(gltf, "mtoon0_render_queue_and_clamp", slider=True)
|
||||
|
||||
reset_op = layout_operator(layout, VRM_OT_reset_mtoon1_material_shader_node_tree)
|
||||
reset_op.material_name = material.name
|
||||
|
||||
|
||||
def draw_material(context: Context, layout: UILayout) -> None:
|
||||
material = context.material
|
||||
if not material:
|
||||
return
|
||||
ext = get_material_extension(material)
|
||||
if ext.mtoon1.is_outline_material:
|
||||
layout.box().label(icon="INFO", text="This is an MToon Outline material")
|
||||
return
|
||||
|
||||
draw_mtoon1_material(context, layout)
|
||||
|
||||
node, legacy_shader_name = search.legacy_shader_node(material)
|
||||
if ext.mtoon1.enabled or (node and legacy_shader_name == "MToon_unversioned"):
|
||||
layout.prop(ext.mtoon1, "export_shape_key_normals")
|
||||
return
|
||||
if node and legacy_shader_name in ["TRANSPARENT_ZWRITE", "GLTF"]:
|
||||
return
|
||||
|
||||
help_column = layout.box().column(align=True)
|
||||
help_message = pgettext(
|
||||
"How to export this material to VRM.\n"
|
||||
+ "Meet one of the following conditions.\n"
|
||||
+ " - VRM MToon Material is enabled\n"
|
||||
+ ' - Connect the "Surface" to a "Principled BSDF"\n'
|
||||
+ ' - Connect the "Surface" to a "MToon_unversioned"\n'
|
||||
+ ' - Connect the "Surface" to a "TRANSPARENT_ZWRITE"\n'
|
||||
+ " - Others that are compatible with the glTF 2.0 add-on export\n"
|
||||
)
|
||||
for index, help_line in enumerate(help_message.splitlines()):
|
||||
help_column.label(
|
||||
text=help_line, translate=False, icon="HELP" if index == 0 else "NONE"
|
||||
)
|
||||
url = "https://docs.blender.org/manual/en/2.93/addons/import_export/scene_gltf2.html#exported-materials"
|
||||
link_row = help_column.split(factor=0.8)
|
||||
link_row.label(text=" " + url, translate=False)
|
||||
web_op = layout_operator(link_row, VRM_OT_open_url_in_web_browser, icon="URL")
|
||||
web_op.url = url
|
||||
|
||||
|
||||
class VRM_PT_vrm_material_property(Panel):
|
||||
bl_idname = "VRM_PT_vrm_material_property"
|
||||
bl_label = "VRM Material"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "material"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return bool(context.material)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
draw_material(context, self.layout)
|
||||
3780
editor/mtoon1/property_group.py
Normal file
3780
editor/mtoon1/property_group.py
Normal file
File diff suppressed because it is too large
Load Diff
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