новая структура проекта

This commit is contained in:
2026-04-20 21:04:25 +04:00
parent 1fe2a72ef1
commit 1a56b22e38
1932 changed files with 1886 additions and 22779 deletions
@@ -0,0 +1,42 @@
import os
import bpy
from pathlib import Path
from .cycles import update_m2_mat_node_tree_cycles
node_groups = [
'EnvMapping',
'UV Picker'
]
def load_m2_shader_dependencies(reload_shader=False):
render_engine = bpy.context.scene.render.engine
# remove old node groups
if reload_shader:
for ng_name in node_groups:
if ng_name in bpy.data.node_groups:
bpy.data.node_groups.remove(bpy.data.node_groups[ng_name])
missing_nodes = [ng_name for ng_name in node_groups if ng_name not in bpy.data.node_groups]
if render_engine in ('CYCLES', 'BLENDER_EEVEE'):
lib_path = os.path.join(str(Path(__file__).parent), 'cycles', 'wotlk_m2_default.blend')
else:
print('\nWARNING: Failed loading shader: materials may not display correctly.'
'\nIncompatible render engine \""{}"\"'.format(render_engine))
return
with bpy.data.libraries.load(lib_path) as (data_from, data_to):
data_to.node_groups = [node_group for node_group in data_from.node_groups if node_group in missing_nodes]
def update_m2_mat_node_tree(bl_mat):
render_engine = bpy.context.scene.render.engine
if render_engine in ('CYCLES', 'BLENDER_EEVEE'):
update_m2_mat_node_tree_cycles(bl_mat)
else:
print('\nWARNING: Failed generating node tree: material \"{}\" may not display correctly.'
'\nIncompatible render engine \""{}"\"'.format(bl_mat.name, render_engine))
@@ -0,0 +1,238 @@
import bpy
from ... import util as util
from ....utils.node_builder import NodeTreeBuilder
BLENDING_MODES_DICT = {
"0": "Opaque",
"1": "AlphaKey",
"2": "Alpha",
"3": "NoAlphaAdd",
"4": "Add",
"5": "Mod",
"6": "Mod2X",
"7": "BlendAdd"
}
def update_material_name(materials):
for material in materials:
tex_1 = material.wow_m2_material.texture_1
tex_2 = material.wow_m2_material.texture_2
tex_1_name = str(tex_1.name).replace('.png', '') if tex_1 else None
tex_2_name = str(tex_2.name).replace('.png', '') if tex_2 else None
texture_1_blending_mode = material.wow_m2_material.texture_1_blending_mode
texture_2_blending_mode = material.wow_m2_material.texture_2_blending_mode
texture_1_blending_mode_name = BLENDING_MODES_DICT.get(str(texture_1_blending_mode), "Unknown")
texture_2_blending_mode_name = BLENDING_MODES_DICT.get(str(texture_2_blending_mode), "Unknown")
if tex_1_name:
if tex_2_name:
material.name = 'T1_{}_({})_T2_{}_({})'.format(
tex_1_name, texture_1_blending_mode_name, tex_2_name, texture_2_blending_mode_name
)
else:
material.name = 'T1_{}_({})'.format(tex_1_name, texture_1_blending_mode_name)
def update_m2_mat_node_tree_cycles(bl_mat):
# -- controller (Empty) that owns the animatable properties
controller = util.find_color_transparency_controller()
# get textures
img_1 = bl_mat.wow_m2_material.texture_1 if bl_mat.wow_m2_material.texture_1 else None
img_2 = bl_mat.wow_m2_material.texture_2 if bl_mat.wow_m2_material.texture_2 else None
def mapping(mapping_method):
if mapping_method == "UVMap":
return 0
elif mapping_method == "UVMap.001":
return 1
elif mapping_method == "Env":
return -1
tex1_uv = mapping(bl_mat.wow_m2_material.texture_1_mapping) if bl_mat.wow_m2_material.texture_1_mapping else 0
tex2_uv = mapping(bl_mat.wow_m2_material.texture_2_mapping) if bl_mat.wow_m2_material.texture_2_mapping else 1
bl_mat.use_nodes = True
tree = bl_mat.node_tree
links = tree.links
tree_builder = NodeTreeBuilder(tree)
uv_picker_node = bpy.data.node_groups.get("UV Picker")
uvmap = tree_builder.add_node('ShaderNodeGroup', 'Tex1_Mapping', 0, 0)
uvmap2 = tree_builder.add_node('ShaderNodeGroup', 'Tex2_Mapping', 0, 1)
uvmap.node_tree = uv_picker_node
uvmap2.node_tree = uv_picker_node
uvmap.inputs[0].default_value = tex1_uv
uvmap2.inputs[0].default_value = tex2_uv
bsdf = tree_builder.add_node('ShaderNodeBsdfPrincipled', 'BSDF', 5, 0)
bsdf.name = 'BSDF'
tex_image = tree_builder.add_node('ShaderNodeTexImage', 'Tex1_image', 1, 0)
tex_image2 = tree_builder.add_node('ShaderNodeTexImage', 'Tex2_image', 1, 1)
if img_1:
tex_image.image = img_1
if img_2:
tex_image2.image = img_2
bsdf.inputs['Specular'].default_value = 0.0
links.new(bsdf.inputs['Base Color'], tex_image.outputs['Color'])
links.new(uvmap.outputs['Result'], tex_image.inputs['Vector'])
links.new(uvmap2.outputs['Result'], tex_image2.inputs['Vector'])
output = tree_builder.add_node("ShaderNodeOutputMaterial", 'Material Output', 6, 0)
links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
alpha_invert = tree_builder.add_node('ShaderNodeInvert', 'Alpha Invert', 2, 0)
alpha_invert.inputs[0].default_value = 1.0
links.new(alpha_invert.inputs[1], tex_image.outputs['Alpha'])
alpha_invert2 = tree_builder.add_node('ShaderNodeInvert', 'Alpha Invert 2', 2, 1)
alpha_invert2.inputs[0].default_value = 1.0
links.new(alpha_invert2.inputs[1], tex_image2.outputs['Alpha'])
tex_mix = tree_builder.add_node('ShaderNodeMix', 'Blending', 3, 0)
tex_mix.data_type = 'RGBA'
tex_mix.inputs[0].default_value = 0.2
tex_mix.blend_type = 'OVERLAY'
links.new(tex_mix.inputs[0], alpha_invert.outputs['Color'])
links.new(tex_mix.inputs[6], tex_image.outputs['Color'])
links.new(tex_mix.inputs[7], tex_image2.outputs['Color'])
tex_alpha_mix = tree_builder.add_node('ShaderNodeMix', 'Alpha_Blending', 3, 1)
tex_alpha_mix.data_type = 'RGBA'
tex_alpha_mix.blend_type = 'SUBTRACT'
links.new(tex_alpha_mix.inputs[0], alpha_invert2.outputs['Color'])
links.new(tex_alpha_mix.inputs[6], tex_image.outputs['Alpha'])
links.new(tex_alpha_mix.inputs[7], tex_image.outputs['Alpha'])
# render flags / culling
if '4' in bl_mat.wow_m2_material.texture_1_render_flags:
bl_mat.use_backface_culling = False
else:
bl_mat.use_backface_culling = True
if img_2:
if '4' in bl_mat.wow_m2_material.texture_2_render_flags or '4' in bl_mat.wow_m2_material.texture_1_render_flags:
bl_mat.use_backface_culling = False
else:
bl_mat.use_backface_culling = True
# blending mode from texture_1
if bl_mat.wow_m2_material.texture_1_blending_mode == '0':
links.new(bsdf.inputs['Alpha'], tex_image.outputs['Alpha'])
links.new(uvmap.outputs['Result'], tex_image.inputs['Vector'])
bl_mat.blend_method = 'OPAQUE'
bl_mat.show_transparent_back = False
if bl_mat.wow_m2_material.texture_1_blending_mode == '1':
links.new(bsdf.inputs['Alpha'], tex_image.outputs['Alpha'])
links.new(uvmap.outputs['Result'], tex_image.inputs['Vector'])
bl_mat.blend_method = 'CLIP'
bl_mat.alpha_threshold = 0.878431
if bl_mat.wow_m2_material.texture_1_blending_mode in {'2', '4'}:
links.new(bsdf.inputs['Alpha'], tex_image.outputs['Alpha'])
links.new(uvmap.outputs['Result'], tex_image.inputs['Vector'])
bl_mat.blend_method = 'BLEND'
# Opaque settings
blending_1 = int(bl_mat.wow_m2_material.texture_1_blending_mode)
tex_image.image.alpha_mode = 'CHANNEL_PACKED'
# ---------------------------
# Transparency (driver)
# ---------------------------
t_mult = tree_builder.add_node('ShaderNodeMath', 'Transparency', 4, 1)
t_mult.operation = 'MULTIPLY'
t_mult.name = 'Transparency'
t_mult.inputs[1].default_value = 1.0
transparency_curve = bl_mat.node_tree.driver_add("nodes[\"Transparency\"].inputs[1].default_value")
driver = transparency_curve.driver
driver.type = 'SCRIPTED'
# which transparency index should this material use?
trans_name = bl_mat.wow_m2_material.transparency # e.g. "Transparency_3"
trans_index = int(''.join(filter(str.isdigit, trans_name))) if trans_name else 0
trans_var = driver.variables.new()
trans_var.name = 'Transparency'
trans_var.targets[0].id_type = 'OBJECT'
trans_var.targets[0].id = controller
trans_var.targets[0].data_path = f'wow_m2_color_transparency.transparencies[{trans_index}].value'
t_mult.label = f'Transparency_{trans_index}_ON'
driver.expression = trans_var.name
# ---------------------------
# Color (drivers)
# ---------------------------
c_mix = tree_builder.add_node('ShaderNodeMix', 'Color', 4, 0)
c_mix.blend_type = 'MULTIPLY'
c_mix.data_type = 'RGBA'
c_mix.name = 'Color'
c_mix.inputs[7].default_value = (1.0, 1.0, 1.0, 1.0)
c_alpha = tree_builder.add_node('ShaderNodeMath', 'Color_Alpha_Mix', 4, 2)
c_alpha.operation = 'MULTIPLY'
c_alpha.name = 'Color_Alpha_Mix'
c_alpha.inputs[1].default_value = 1.0
color_components = ['R', 'G', 'B']
color_name = bl_mat.wow_m2_material.color # e.g. "Color_5"
if color_name != "":
color_index = int(''.join(filter(str.isdigit, color_name)))
c_mix.label = f'Color_{color_index}_ON'
c_mix.inputs[0].default_value = 1.0
c_alpha.label = f'Color_{color_index}_Alpha_ON'
else:
color_index = 0
c_mix.label = 'Color_0_OFF'
c_mix.inputs[0].default_value = 0.0
c_alpha.label = 'Color_0_Alpha_OFF'
# RGB components from controller.colors[color_index].color[0..2]
for i, component in enumerate(color_components):
color_curve = bl_mat.node_tree.driver_add('nodes["Color"].inputs[7].default_value', i)
c_driver = color_curve.driver
c_driver.type = 'SCRIPTED'
c_driver.expression = component # variable name
c_var = c_driver.variables.new()
c_var.name = component
c_var.targets[0].id_type = 'OBJECT'
c_var.targets[0].id = controller
c_var.targets[0].data_path = f'wow_m2_color_transparency.colors[{color_index}].color[{i}]'
# Alpha from controller.colors[color_index].alpha
color_alpha_curve = bl_mat.node_tree.driver_add('nodes["Color_Alpha_Mix"].inputs[1].default_value')
color_a_driver = color_alpha_curve.driver
color_a_driver.type = 'SCRIPTED'
color_a_driver.expression = 'Alpha'
alpha_var = color_a_driver.variables.new()
alpha_var.name = 'Alpha'
alpha_var.targets[0].id_type = 'OBJECT'
alpha_var.targets[0].id = controller
alpha_var.targets[0].data_path = f'wow_m2_color_transparency.colors[{color_index}].alpha'
# Pipe alpha/color
if color_name != "":
links.new(c_alpha.inputs['Value'], t_mult.outputs['Value'])
links.new(c_alpha.outputs['Value'], bsdf.inputs['Alpha'])
else:
links.new(t_mult.outputs['Value'], bsdf.inputs['Alpha'])
# BaseColor & final alpha selection
if not img_2:
links.new(tex_image.outputs[0], c_mix.inputs[6])
links.new(tex_image.outputs['Alpha'], t_mult.inputs['Value'])
else:
links.new(tex_mix.outputs[2], c_mix.inputs[6])
links.new(tex_alpha_mix.outputs[2], t_mult.inputs['Value'])
links.new(bsdf.inputs['Base Color'], c_mix.outputs[2])
@@ -0,0 +1,60 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://b843845myvgai"
path="res://.godot/imported/wotlk_m2_default.blend-e075a075ddec76c7966149d9edc95f54.scn"
[deps]
source_file="res://reference/blender-wow-studio-3.4-1.1.0_Experimental/io_scene_wmo/m2/bl_render/cycles/wotlk_m2_default.blend"
dest_files=["res://.godot/imported/wotlk_m2_default.blend-e075a075ddec76c7966149d9edc95f54.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
blender/nodes/visible=0
blender/nodes/active_collection_only=false
blender/nodes/punctual_lights=true
blender/nodes/cameras=true
blender/nodes/custom_properties=true
blender/nodes/modifiers=1
blender/meshes/colors=false
blender/meshes/uvs=true
blender/meshes/normals=true
blender/meshes/export_geometry_nodes_instances=false
blender/meshes/gpu_instances=false
blender/meshes/tangents=true
blender/meshes/skins=2
blender/meshes/export_bones_deforming_mesh_only=false
blender/materials/unpack_enabled=true
blender/materials/export_materials=1
blender/animation/limit_playback=true
blender/animation/always_sample=true
blender/animation/group_tracks=true
gltf/naming_version=2
@@ -0,0 +1,123 @@
import os
import time
import importlib
import traceback
import bpy
from ..pywowlib.m2_file import M2File
from ..third_party.tqdm import tqdm
from ..ui.preferences import get_project_preferences
from ..utils.misc import resolve_outside_model_path
from . import m2_scene
from .operations import m2_action_logger as log
def create_m2(version, filepath, selected_only, fill_textures, forward_axis, scale, merge_vertices):
"""
Creates and prepares an M2 structure from Blender data for export.
Executes all necessary pipeline steps to build and validate the model.
"""
# --- Load project settings and initialize M2 container ---
proj_prefs = get_project_preferences()
time_import_method = proj_prefs.time_import_method
m2 = M2File(version)
# Reload M2 scene builder to ensure latest code
importlib.reload(m2_scene)
bl_m2 = m2_scene.BlenderM2Scene(m2, proj_prefs)
# Resolve internal game path for export
export_path = resolve_outside_model_path(filepath)
if export_path:
bpy.context.scene.wow_scene.game_path = export_path
print("\n\n##########################")
print("### Exporting M2 model ###")
print("##########################")
print("\n")
# --- Clear logging system ---
log.clear()
start_time = time.time()
# --- Ordered export processing steps ---
steps = [
("prepare_export_axis", lambda: bl_m2.prepare_export_axis(forward_axis, scale)),
("prepare_pose", lambda: bl_m2.prepare_pose(selected_only)),
("save_properties", lambda: bl_m2.save_properties(filepath, selected_only)),
("save_bones", lambda: bl_m2.save_bones(selected_only)),
("save_cameras", bl_m2.save_cameras),
("save_attachments", bl_m2.save_attachments),
("save_events", bl_m2.save_events),
("save_lights", bl_m2.save_lights),
("save_ribbons", bl_m2.save_ribbons),
("save_particles", lambda: bl_m2.save_particles(time_import_method)),
("save_animations", lambda: bl_m2.save_animations(time_import_method)),
("save_geosets", lambda: bl_m2.save_geosets(selected_only, fill_textures, merge_vertices)),
("save_collision", lambda: bl_m2.save_collision(selected_only)),
("restore_pose", bl_m2.restore_pose),
]
# --- Execute each export step sequentially ---
for step_name, func in tqdm(steps, desc="Exporting M2 Steps", ascii=True):
try:
log.debug(f"Starting export step: {step_name}")
func()
log.debug(f"Completed step: {step_name}")
except Exception as e:
tb = traceback.format_exc()
log.error(f"Export step '{step_name}' failed: {e}\n{tb}")
continue
# --- Log final status ---
log.info(
f"Successfully exported M2 to '{filepath}' "
f"(Total export time: {time.strftime('%M minutes %S seconds', time.gmtime(time.time() - start_time))})"
)
warnings, errors = log.print_export_log()
# Display viewport notification based on result
if errors:
bpy.ops.wbs.viewport_text_display(
'INVOKE_DEFAULT',
message="ERROR: M2 Export Failed! Check console!",
font_size=32, y_offset=120, color=(1, 0, 0, 1)
)
return None
elif warnings:
bpy.ops.wbs.viewport_text_display(
'INVOKE_DEFAULT',
message="WARNING: M2 Exported with Warnings, check console!",
font_size=28, y_offset=100, color=(1, 0.15, 0.15, 1)
)
else:
bpy.ops.wbs.viewport_text_display(
'INVOKE_DEFAULT',
message="Info: Successfully exported M2!",
font_size=24, y_offset=67
)
# --- Clear logging system ---
log.clear()
return m2
def export_m2(version, filepath, selected_only, fill_textures, forward_axis, scale, merge_vertices):
"""
Exports an M2 file to disk.
Removes old file, builds M2 data, and writes the final model output.
"""
# Create and assemble M2 structure
m2 = create_m2(version, filepath, selected_only, fill_textures, forward_axis, scale, merge_vertices)
if not m2:
raise Exception("Export aborted due to critical errors.")
# Remove existing file to avoid corruption
if os.path.exists(filepath):
os.remove(filepath)
# Write M2 to disk
m2.write(filepath)
@@ -0,0 +1,237 @@
import os
import struct
import time
import importlib
import traceback
import bpy
from ..pywowlib.m2_file import M2File, M2Versions
from ..third_party.tqdm import tqdm
from ..ui.preferences import get_project_preferences
from ..utils.misc import load_game_data
from . import m2_scene
from .operations import m2_action_logger as log
from . import util as util
def import_m2(version, filepath, is_local_file, time_import_method):
"""
Imports a World of Warcraft M2 model into Blender.
Loads game data, extracts required M2 dependencies, processes imported assets,
and builds the Blender object hierarchy.
"""
start_time = time.time()
print("\n\n##########################")
print("### Importing M2 model ###")
print("##########################")
print("\n")
# --- Clear logging system ---
log.clear()
# --- Get project and game data ---
project_preferences = get_project_preferences()
try:
game_data = load_game_data()
except UserWarning:
game_data = None
m2_file = M2File(version, filepath=filepath)
m2 = m2_file.root
m2.filepath = filepath # Temporary assignment workaround
extract_dir = os.path.dirname(filepath) if is_local_file else project_preferences.cache_dir_path
if not extract_dir:
raise Exception('Error: cache directory is not specified. Check addon settings.')
# --- Extract M2 asset files ---
if game_data and game_data.files:
log.info("Extracting M2 required files into cache folder")
dependencies = m2_file.find_model_dependencies()
# Extract textures
m2_file.texture_path_map = game_data.extract_textures_as_png(project_preferences.cache_dir_path, dependencies.textures)
# Extract animations
anim_filepaths = {}
for key, identifier in dependencies.anims.items():
if is_local_file:
full_path = os.path.join(extract_dir, os.path.split(identifier)[-1])
if os.path.exists(full_path):
anim_filepaths[key] = full_path
else:
anim_filepaths[key] = os.path.split(identifier)[-1]
print("\n.anim not found at:", full_path, '\n')
else:
try:
anim_filepaths[key] = game_data.extract_file(extract_dir, identifier, 'anim')
except:
anim_filepaths[key] = os.path.split(identifier)[-1]
print("\nFailed to extract anim from game data:", identifier)
# Extract skin and supporting files
skin_filepaths = dependencies.skins if is_local_file else game_data.extract_files(extract_dir, dependencies.skins, 'skin')
if version >= M2Versions.WOD:
game_data.extract_files(extract_dir, dependencies.bones, 'bone', True)
game_data.extract_files(extract_dir, dependencies.lod_skins, 'skin', True)
else:
raise NotImplementedError('Error: Importing without gamedata loaded is not yet implemented.')
# --- Cleanup and load additional M2 data ---
skin_filepaths = [p for p in skin_filepaths if p]
if isinstance(anim_filepaths, dict):
anim_filepaths = {k: v for k, v in anim_filepaths.items() if v is not None}
try:
m2_file.read_additional_files(skin_filepaths, anim_filepaths)
except KeyError as e:
log.warn(f"Missing animation {e.args[0]} - skipping.")
m2_file.root.assign_bone_names()
if not is_local_file:
for key, identifier in dependencies.anims.items():
path_to_remove = os.path.join(extract_dir, identifier)
if os.path.exists(path_to_remove):
os.remove(path_to_remove)
# --- Reload M2 scene handler and prepare Blender scene ---
importlib.reload(m2_scene)
bl_m2 = m2_scene.BlenderM2Scene(m2_file, project_preferences)
cache_dir = project_preferences.cache_dir_path
end_index = filepath.find(cache_dir) + len(cache_dir)
m2_filepath = filepath[end_index:]
if not is_local_file:
bpy.context.scene.wow_scene.game_path = m2_filepath
else:
normalized_path = os.path.normpath(filepath)
path_parts = [part.lower() for part in normalized_path.split(os.sep)]
wow_root_folders = ["character", "creature", "environments", "item", "spells", "world"]
base_path_index = next((path_parts.index(cat) for cat in wow_root_folders if cat in path_parts), 0)
bpy.context.scene.wow_scene.game_path = os.sep.join(path_parts[base_path_index:])
log.debug(f"Normalized path '{normalized_path}'")
# --- Create Blender collection for M2 object ---
m2_name = os.path.splitext(os.path.basename(filepath))[0]
main_collection, collections = util.get_or_create_m2_collection(m2_name)
# --- Import pipeline steps ---
steps = [
("load_armature", bl_m2.load_armature, None, collections["armature"], []),
("load_animations", bl_m2.load_animations, None, None, []),
("load_colors", bl_m2.load_colors, None, collections["color_transparency"], [time_import_method]),
("load_transparency", bl_m2.load_transparency, None, collections["color_transparency"], [time_import_method]),
("load_materials", bl_m2.load_materials, "dbc_textures", None, []),
("load_geosets", bl_m2.load_geosets, None, collections["geosets"], []),
("load_texture_transforms", bl_m2.load_texture_transforms, None, collections["texture_transforms"], []),
("load_collision", bl_m2.load_collision, None, collections["collision"], []),
("load_attachments", bl_m2.load_attachments, None, collections["attachments"], []),
("load_lights", bl_m2.load_lights, None, collections["lights"], []),
("load_events", bl_m2.load_events, None, collections["events"], []),
("load_cameras", bl_m2.load_cameras, None, collections["cameras"], [time_import_method]),
("load_ribbons", bl_m2.load_ribbons, None, collections["ribbons"], []),
("load_particles", bl_m2.load_particles, None, collections["particles"], [time_import_method]),
("load_globalflags", bl_m2.load_globalflags, None, main_collection, []),
]
# --- Execute import steps ---
results = {}
for step_name, func, result_key, target_collection, extra_args in tqdm(steps, total=len(steps), desc="Importing M2 Steps", ascii=True):
try:
log.debug(f"Starting import step: {step_name}")
args = []
if target_collection:
args.append(target_collection)
if extra_args:
args.extend(extra_args)
result = func(*args)
log.debug(f"Completed import step: {step_name}")
if result_key:
results[result_key] = result
except Exception as e:
tb = traceback.format_exc()
log.error(f"Import step '{step_name}' failed: {e}\n{tb}")
continue
# --- Handle results ---
dbc_textures = results.get("dbc_textures")
if dbc_textures:
bpy.ops.scene.wow_creature_load_textures(LoadAll=True)
log.info(
f"Done importing M2. (Total import time: {time.strftime('%M minutes %S seconds.', time.gmtime(time.time() - start_time))})"
)
warnings, errors = log.print_import_log()
if errors:
bpy.ops.wbs.viewport_text_display(
'INVOKE_DEFAULT', message="ERROR: M2 Import Failed! Check console!", font_size=32, y_offset=120, color=(1, 0, 0, 1)
)
return None
elif warnings:
bpy.ops.wbs.viewport_text_display(
'INVOKE_DEFAULT', message="WARNING: M2 Imported with Warnings, check console!", font_size=28, y_offset=100, color=(1, 0.15, 0.15, 1)
)
else:
bpy.ops.wbs.viewport_text_display(
'INVOKE_DEFAULT', message="Info: Successfully imported M2!", font_size=24, y_offset=67
)
log.clear()
return m2_file
def import_m2_gamedata(version, filepath, is_local_file):
"""
Imports an M2 directly from game data storage.
Extracts the M2 and skin files into the cache, runs the M2 import routine,
and cleans up extracted files after processing.
"""
game_data = load_game_data()
if not game_data or not game_data.files:
raise FileNotFoundError("Game data is not loaded.")
addon_prefs = get_project_preferences()
cache_dir = addon_prefs.cache_dir_path
time_import_method = addon_prefs.time_import_method
# --- Configure scene timing based on import mode ---
if time_import_method == 'Convert':
bpy.context.scene.render.fps = 30
bpy.context.scene.sync_mode = 'NONE'
else:
bpy.context.scene.render.fps = 1000
bpy.context.scene.sync_mode = 'FRAME_DROP'
# --- Extract and prepare core M2 file ---
game_data.extract_file(cache_dir, filepath)
if os.name != 'nt':
filepath = filepath.lower()
root_path = os.path.join(cache_dir, filepath.replace('\\', '/'))
else:
root_path = os.path.join(cache_dir, filepath)
# --- Determine number of skins ---
with open(root_path, 'rb') as f:
f.seek(68)
n_skins = struct.unpack('I', f.read(4))[0]
skin_paths = [f"{filepath[:-3]}{str(i).zfill(2)}.skin" for i in range(n_skins)]
game_data.extract_files(cache_dir, skin_paths)
# --- Import complete M2 ---
import_m2(version, root_path, is_local_file, time_import_method)
# --- Cleanup extracted files ---
os.remove(root_path)
for skin_path in skin_paths:
os.remove(os.path.join(cache_dir, *skin_path.split('\\')))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,185 @@
import bpy
import re
from mathutils import Matrix, Vector, Quaternion
from ..util import can_apply_scale,make_fcurve_compound,get_bone_groups
def convert_m2_bones():
def fix_scale(matrix,curves,keyframe_count):
if not can_apply_scale(curves,keyframe_count):
return (True,'Non-uniform scaling')
for i in range(keyframe_count):
def co(j): return fcurves[j].keyframe_points[i].co
# read vector defining the old rotation
vec = Vector((co(0)[1], co(1)[1], co(2)[1]))
# TODO: CHANGE VECTOR USING 'matrix' HERE SOMEHOW
# write vector back
co(0)[1] = vec.x
co(1)[1] = vec.y
co(2)[1] = vec.z
return (False,'')
def fix_rotation(matrix,fcurves,keyframe_count):
def quat_dist(q1,q2):
# takes polarity into account on purpose.
# we just want to do _mostly_ correct rotations,
# but there might be a better formula to use here.
return (
pow(q1.w-q2.w,2) +
pow(q1.x-q2.x,2) +
pow(q1.y-q2.y,2) +
pow(q1.z-q2.z,2))
last_quat = None
for i in range(keyframe_count):
def co(j): return fcurves[j].keyframe_points[i].co
q_in = Quaternion((co(0)[1], co(1)[1], co(2)[1], co(3)[1]))
axis,angle = q_in.to_axis_angle()
axis.rotate(matrix)
rot_q = Quaternion(axis,angle)
if last_quat is None:
last_quat = rot_q
else:
rot_q_neg = Quaternion(-rot_q)
dist = quat_dist(rot_q,last_quat)
neg_dist = quat_dist(rot_q_neg,last_quat)
last_quat = rot_q if dist <= neg_dist else rot_q_neg
co(0)[1] = last_quat.w
co(2)[1] = last_quat.x
co(1)[1] = -last_quat.y
co(3)[1] = last_quat.z
return (False,'')
def fix_location(matrix, fcurves,keyframe_count):
for i in range(keyframe_count):
def co(j): return fcurves[j].keyframe_points[i].co
vec = Vector((co(0)[1],co(1)[1], co(2)[1]))
vec.rotate(matrix)
co(1)[1] = vec.x
co(0)[1] = -vec.y
co(2)[1] = vec.z
return (False,'')
def fix_curves(name, matrix, fcurves, track_count, callback):
for i in range(track_count):
if not i in fcurves:
raise ValueError(f'Track index {i} missing in {name} fcurves')
keyframe_count = len(fcurves[0].keyframe_points)
for i,fcurve in fcurves.items():
cur_count = len(fcurve.keyframe_points)
if cur_count != keyframe_count:
raise ValueError(f'Track index {i} keyframe count ({cur_count}) is different from index 0 {keyframe_count}')
for i in range(keyframe_count):
time = fcurves[0].keyframe_points[i].co[0]
for j in range(track_count):
cur_time = fcurves[j].keyframe_points[i].co[0]
if cur_time != time:
raise ValueError(f'Track index {j} frame {j} has a different time value ({cur_time}) from index 0 ({time})')
return callback(matrix,fcurves,keyframe_count)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
fixed_vertices = 0
for obj in bpy.data.objects:
if obj.type != 'MESH' or obj.parent is None or obj.parent.type != 'ARMATURE':
continue
bone_names = [bone.name for bone in obj.parent.data.bones]
for vertex in obj.data.vertices:
groups = get_bone_groups(obj, vertex, bone_names)
for el in groups[4:]:
obj.vertex_groups[el.group].remove([vertex.index])
if len(groups) > 4:
fixed_vertices += 1
print(f'Removed overflowing groups for {fixed_vertices} vertices')
for action in bpy.data.actions:
removed_fcurves = []
for curve in action.fcurves:
if curve.data_path in ["location","rotation_euler","scale"]:
removed_fcurves.append(curve)
print(f'Removed fcurve "{curve.data_path}[{curve.array_index}]" from action {action.name}')
for curve in removed_fcurves:
action.fcurves.remove(curve)
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.transform_apply(location=True, rotation=True, scale=False)
bpy.ops.object.select_all(action='DESELECT')
changed_bones = {}
changed_objects = []
for obj in bpy.data.objects:
if obj.type != 'ARMATURE': continue
try:
obj.select_set(True)
except RuntimeError as e:
print(f"Unable to select armature {obj.name}, not converting it")
continue
changed_objects.append(obj)
#this was enabled in 3.0 and worked better i think
bpy.ops.object.mode_set(mode='EDIT')
for bone in obj.data.edit_bones:
if bone.use_connect:
bone.use_connect = False
for bone in obj.data.edit_bones:
bone.use_connect = False
bone.roll = 0
bone.tail = bone.head + Vector((1,0,0))
changed_bones[bone.name] = Matrix(obj.data.bones[bone.name].matrix_local)
bpy.ops.object.mode_set(mode='OBJECT')
for action in bpy.data.actions:
fcurve_compounds = make_fcurve_compound(action.fcurves,
lambda path: path.startswith('pose.bones')
)
for key,fcurves in fcurve_compounds.items():
bone = re.search('"(.+?)"',key).group(1)
if not bone in changed_bones:
continue
matrix = changed_bones[bone]
curve_type = re.search('([a-zA-Z_]+)$',key).group(0)
remove_reason = None
should_remove = False
if curve_type == 'scale':
(should_remove,remove_reason) = fix_curves(key,matrix,fcurves,3,fix_scale)
if curve_type == 'location':
(should_remove,remove_reason) = fix_curves(key,matrix,fcurves,3,fix_location)
if curve_type == 'rotation_quaternion':
(should_remove,remove_reason) = fix_curves(key,matrix,fcurves,4,fix_rotation)
if should_remove:
print(f"Deleting incompatible fcurves {fcurve.data_path}: {remove_reason}")
for fcurve in fcurves.values():
action.fcurves.remove(fcurve)
for obj in changed_objects:
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.ops.object.mode_set(mode='POSE')
# clear other curves if needed
bpy.ops.pose.scale_clear()
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
else:
for fcurve in fcurves.values():
fcurve.update()
@@ -0,0 +1,157 @@
from datetime import datetime
from enum import IntEnum
from ...ui.preferences import get_project_preferences
from . import m2_export_validation
class LogLevel(IntEnum):
"""Logging severity levels."""
ERROR = 1
WARN = 2
INFO = 3
DEBUG = 4
# ---------------------------
# Storage
# ---------------------------
errors = []
warnings = []
infos = []
debugs = []
master_log = []
# ---------------------------
# Utility
# ---------------------------
def clear():
"""Reset all stored logs."""
warnings.clear()
infos.clear()
errors.clear()
debugs.clear()
master_log.clear()
def get_verbosity_level() -> int:
"""
Retrieve verbosity setting from addon preferences.
Defaults to INFO level (3) if unavailable.
"""
try:
prefs = get_project_preferences()
return int(prefs.verbosity_level)
except Exception:
return 3 # INFO fallback
# ---------------------------
# Core logging
# ---------------------------
def add(level: LogLevel, msg: str, print_now: bool = False):
"""
Store a log entry and optionally print immediately.
Args:
level (LogLevel): severity of message
msg (str): log message string
print_now (bool): force console print immediately
"""
verbosity = get_verbosity_level()
timestamp = datetime.now().strftime("%H:%M:%S")
# --- Record per-level ---
if level == LogLevel.ERROR:
errors.append(msg)
elif level == LogLevel.WARN:
warnings.append(msg)
elif level == LogLevel.INFO:
infos.append(msg)
elif level == LogLevel.DEBUG:
debugs.append(msg)
# --- Append chronological record ---
master_log.append((level, msg, timestamp))
# --- Live print if enabled ---
if print_now and verbosity >= level:
print(f"[{timestamp}] [{level.name}] {msg}")
def _print_log_summary(title: str, validation_fn=None):
"""
Print combined summary for import/export logs.
Args:
title (str): label ("Import" or "Export")
validation_fn (callable, optional): optional validation hook
Returns:
tuple(bool, bool): (warnings_present, errors_present)
"""
verbosity = get_verbosity_level()
print("\n##############################################################")
print(f" {title} Log Summary")
print("##############################################################")
# --- Optional validation ---
if callable(validation_fn):
for name, descriptions, items in validation_fn():
print(f"\n== {name} ==")
for d in descriptions:
print(f" {d}")
for item in items:
add(LogLevel.WARN, f"[{name}] {item}", print_now=True)
# --- Errors & Warnings ---
if errors:
print(f"\n== {title} Errors ==")
for msg in errors:
print(f" [ERROR] {msg}")
if warnings:
print(f"\n== {title} Warnings ==")
for msg in warnings:
print(f" [WARN] {msg}")
# --- Full chronological log ---
print(f"\n== {title} Log ==")
for level, msg, timestamp in master_log:
if verbosity >= level:
print(f" [{timestamp}] [{level.name}] {msg}")
# --- Summary footer ---
print("\n##############################################################")
print(f" Summary: {len(errors)} errors, {len(warnings)} warnings, "
f"{len(infos)} info, {len(debugs)} debug messages")
print("##############################################################\n")
return bool(warnings), bool(errors)
def print_export_log():
"""
Print export summary and run export validation checks.
Returns: (warnings_present, errors_present)
"""
return _print_log_summary("Export", validation_fn=m2_export_validation.run_validations)
def print_import_log():
"""
Print import summary only.
Returns: (warnings_present, errors_present)
"""
return _print_log_summary("Import")
# ---------------------------
# Shorthands
# ---------------------------
error = lambda msg, print_now=False, **kw: add(LogLevel.ERROR, msg, print_now=print_now, **kw)
warn = lambda msg, print_now=False, **kw: add(LogLevel.WARN, msg, print_now=print_now, **kw)
info = lambda msg, print_now=False, **kw: add(LogLevel.INFO, msg, print_now=print_now, **kw)
debug = lambda msg, print_now=False, **kw: add(LogLevel.DEBUG, msg, print_now=print_now, **kw)
@@ -0,0 +1,255 @@
import bpy
from ..util import get_bone_groups
# ---------------------------
# Scene validation functions
# ---------------------------
def wrong_scene_type():
name = "Wrong Scene Type"
description = [
"Issue: The scene type is set to WMO instead of M2",
"Fix: Change the scene type to 'M2' in the top-right corner of Blender"
]
items = []
if not bpy.context.scene:
items.append("Wrong scene: There is no scene object, please report this (unexpected).")
elif bpy.context.scene.wow_scene.type != 'M2':
items.append(f"Wrong scene: Type is {bpy.context.scene.wow_scene.type} but should be M2")
return (name, description, items)
def transformed_objects():
name = "Transformed Objects"
description = [
"Issue: Objects in the scene are transformed (moved, rotated, or scaled).",
"Fix: Run the 'Convert Bones To WoW' command and fix any issues it causes."
]
items = []
def vec_eq(n, q1, q2):
for i in range(n):
if q1[i] != q2[i]:
return False
return True
def vec_str(value, names):
return " ".join([f"{names[i]}={value[i]}" for i in range(len(names))])
for obj in bpy.data.objects:
if obj.type not in ('ARMATURE', 'MESH'):
continue
def compare(name, names, val1, val2):
if not vec_eq(len(names), val1, val2):
items.append(f"Object {obj.name}'s {name} is {vec_str(val1, names)}, but should be {vec_str(val2, names)}")
vec_names = ['x', 'y', 'z']
quat_names = ['w', 'x', 'y', 'z']
compare("location", vec_names, obj.location, (0, 0, 0))
compare("scale", vec_names, obj.scale, (1, 1, 1))
if obj.rotation_mode == 'QUATERNION':
compare("quaternion rotation", quat_names, obj.rotation_quaternion, (1, 0, 0, 0))
elif obj.rotation_mode == 'AXIS_ANGLE':
compare("axis angle rotation", quat_names, obj.rotation_quaternion, (1, 0, 0, 0))
else:
compare("euler rotation", vec_names, obj.rotation_euler, (0, 0, 0))
return (name, description, items)
def empty_textures():
name = "Empty Textures"
description = [
"Issue: An M2 material has no texture set in any of its texture slots.",
"Effect: Will usually cause the model to become invisible ingame.",
"Note: This is not *always* an error, not all materials have textures."
]
items = []
for obj in bpy.data.objects:
if not hasattr(obj, "wow_m2_geoset") or obj.wow_m2_geoset.collision_mesh or not obj.material_slots:
continue
for slot in obj.material_slots:
mat = slot.material
if mat is None:
items.append(f"Object '{obj.name}' has an empty material slot.")
continue
if not hasattr(mat, "wow_m2_material"):
items.append(f"Material '{mat.name}' on object '{obj.name}' has no wow_m2_material attribute.")
continue
wow_mat = mat.wow_m2_material
if not hasattr(wow_mat, "texture_1"):
items.append(f"wow_m2_material on '{mat.name}' has no 'texture_1' attribute.")
continue
if wow_mat.texture_1 is None:
items.append(f"Object '{obj.name}', material '{mat.name}' has no M2 textures, may be invisible ingame.")
return (name, description, items)
def empty_texture_paths():
name = "Empty Texture Path"
description = [
"Issue: A model has an M2 material with a texture set that has no .blp path.",
"Effect: Will usually cause the model to become invisible ingame.",
"Fix: Find the material with the texture and fill the Texture Path.",
]
items = []
texture_maps = {}
for obj in bpy.data.objects:
if obj.type == 'MESH' and not obj.wow_m2_geoset.collision_mesh and obj.material_slots:
for slot in obj.material_slots:
if slot.material is None or not hasattr(slot.material, 'wow_m2_material'):
continue
mat = slot.material.wow_m2_material
for texture in [mat.texture_1, mat.texture_2]:
if texture and hasattr(texture, 'wow_m2_texture') and not texture.wow_m2_texture.path:
if texture.wow_m2_texture.texture_type == '0':
texture_maps.setdefault(texture.name, []).append(obj.name)
for texture, obj_names in texture_maps.items():
obj_str = ", ".join(obj_names)
items.append(f"Texture '{texture}' used by ({obj_str}) has no .blp path set.")
return (name, description, items)
def no_materials():
name = "No Materials"
description = [
"Issue: A model has no materials set.",
"Effect: The model will usually be invisible ingame.",
"Fix: Add at least one material to your model.",
"Note: Not always an error, some models don't require materials."
]
items = [
f"Object {obj.name} has no M2 materials (invisible ingame)."
for obj in bpy.data.objects
if obj.type == 'MESH' and not obj.wow_m2_geoset.collision_mesh and len(obj.material_slots) == 0
]
return (name, description, items)
def bone_constraints():
name = "Bone Constraints"
description = [
"Issue: A bone has constraints applied.",
"Effect: Will almost always break animations (WoW doesn't support bone constraints).",
"Fix: Remove bone constraints or bake animations into keyframes."
]
items = []
for obj in bpy.data.objects:
if obj.type != 'ARMATURE':
continue
for bone in obj.pose.bones:
for constraint in bone.constraints:
items.append(f"Bone {obj.name}.{bone.name} has constraint '{constraint.name}', usually a mistake.")
return (name, description, items)
def no_animation_pairs():
name = "No Animation Pairs"
description = [
"Issue: Animations in the Animation Editor have no object pairs.",
"Effect: No animation data will be exported for this sequence.",
"Fix: Add at least one object/action pair."
]
items = [
f"Sequence '{seq.name}' has no pairs."
for seq in bpy.context.scene.wow_m2_animations
if len(seq.anim_pairs) == 0 and "64" not in seq.flags
]
return (name, description, items)
def missing_animation_items():
name = "Missing Animation Items"
description = [
"Issue: Animation pairs lack an object or action.",
"Effect: No animation data will be written.",
"Fix: Assign both an object and an action."
]
items = []
for seq in bpy.context.scene.wow_m2_animations:
for pair in seq.anim_pairs:
if pair.object is None:
items.append(f"Sequence '{seq.name}' pair missing object.")
elif pair.action is None:
if pair.object.name not in ['CharInfoCam', 'CharInfoCam_Target', 'PortraitCam', 'PortraitCam_Target']:
items.append(f"Sequence '{seq.name}' pair '{pair.object.name}' missing action.")
return (name, description, items)
def non_primary_sequences():
name = "Non-primary Sequences"
description = [
"Issue: Non-primary sequences are unsupported.",
"Effect: Animation will break or crash the game.",
"Fix: Add the 'primary sequence' flag."
]
items = [
f"Sequence '{seq.name}' is missing the primary flag."
for seq in bpy.context.scene.wow_m2_animations
if not seq.is_global_sequence and "32" not in seq.flags
]
return (name, description, items)
def too_many_bone_groups():
name = "Too Many Bone Groups"
description = [
"Issue: Vertices are influenced by more than 4 bones.",
"Effect: Excess bones will be dropped, mesh will deform incorrectly.",
"Fix: Use 'Limit Bone Groups' or test the effect in-game."
]
items = []
for obj in bpy.data.objects:
if obj.type != 'MESH' or not obj.parent or obj.parent.type != 'ARMATURE':
continue
bone_names = [b.name for b in obj.parent.data.bones]
broken = sum(1 for v in obj.data.vertices if len(get_bone_groups(obj, v, bone_names)) > 4)
if broken > 0:
items.append(f"Object '{obj.name}' has {broken} vertices with too many bone groups.")
return (name, description, items)
def fcurves_transforming_objects():
name = "FCurves Transforming Objects"
description = [
"Issue: FCurves animate Blender objects directly, unsupported in M2.",
"Effect: Object transforms incorrectly in-game.",
"Fix: Run 'Convert Bones To WoW' and verify results."
]
items = []
for animation in bpy.context.scene.wow_m2_animations:
for pair in animation.anim_pairs:
if not pair.action:
continue
for curve in pair.action.fcurves:
if curve.data_path in ["location", "rotation_euler", "scale"]:
if pair.object and not pair.object.wow_m2_uv_transform.enabled:
items.append(f"FCurve '{curve.data_path}[{curve.array_index}]' in '{pair.action.name}' transforms object '{pair.object.name}'.")
return (name, description, items)
def run_validations():
"""Run all static validation checks and return structured results."""
validation_callbacks = [
wrong_scene_type,
transformed_objects,
empty_textures,
empty_texture_paths,
no_materials,
bone_constraints,
no_animation_pairs,
missing_animation_items,
non_primary_sequences,
too_many_bone_groups,
fcurves_transforming_objects,
]
results = []
for callback in validation_callbacks:
name, descriptions, items = callback()
if items:
results.append((name, descriptions, items))
return results
@@ -0,0 +1,71 @@
import bpy
__reload_order_index__ = -1
###############################
## Camera Animation Driver Utils
###############################
def update_frame_range(obj):
last_frame = 0
for segment in obj.wow_m2_camera.animation_curves:
first_frame = last_frame
last_frame = first_frame + segment.duration
segment.frame_start, segment.frame_end = first_frame, last_frame
def in_path_segment(constraint, obj, frame):
segment = constraint.target
if not segment:
'''
raise Exception('\nConstraint \"{}\" does not have a target or the target is invalid.'
' Path animation cannot be evaluated.'.format(constraint.name))
'''
pass
frame_start = 0
frame_end = 0
for curve in obj.wow_m2_camera.animation_curves:
frame_start = frame_end
frame_end = frame_start + curve.duration
if segment == curve.object:
break
return frame_start <= frame < frame_end
def calc_segment_offset(constraint, obj, frame):
segment = constraint.target
if not segment:
'''
raise Exception('\nConstraint \"{}\" does not have a target or the target is invalid.'
' Path animation cannot be evaluated.'.format(constraint.name))
'''
pass
frame_end = 0
for curve in obj.wow_m2_camera.animation_curves:
frame_start = frame_end
frame_end = frame_start + curve.duration
if segment == curve.object:
if not curve.duration:
return 0
return (frame - frame_start) / curve.duration
return 0
def register():
bpy.app.driver_namespace["in_path_segment"] = in_path_segment
bpy.app.driver_namespace["calc_segment_offset"] = calc_segment_offset
@@ -0,0 +1,595 @@
from ...utils.misc import load_game_data
from ... import ui_icons
from ...pywowlib.enums.m2_enums import *
__reload_order_index__ = -1
###############################
## Enumerated constants
###############################
GLOBAL_FLAGS = [
("1","Tilt X", "Model will tilt according to terrain on X axis", "PMARKER", 0x1),
("2","Tilt Y", "Model will tilt according to terrain on Y axis", "PMARKER", 0x2),
("4","Unknown", "", "QUESTION", 0x4),
("8","Texture Combiner", "Add textureCombinerCombos array to end of data", "PMARKER", 0x8),
("16","Unknown", "", "QUESTION", 0x10),
("32","Load Phys Data Mop", "", "PMARKER", 0x20),
("64","Unknown", "", "QUESTION", 0x40),
("128","Unknown", "with this flag unset, demon hunter tattoos stop glowing // since Cata (4.0.1.12911) every model now has this flag", "QUESTION", 0x80),
("256","Camera Related", "", "QUESTION", 0x100),
("512","New Particle Record", "In CATA: new version of ParticleEmitters", "PMARKER", 0x200),
("1024","Unknown", "", "QUESTION", 0x400),
("2048","Texture Transforms Use Bone Sequences", "When set, texture transforms are animated using the sequence being played on the bone found by index in tex_unit_lookup_table[textureTransformIndex], instead of using the sequence being played on the model's first bone", "PMARKER", 0x800),
("4096","Unknown", "", "QUESTION", 0x1000),
("8192","ChunkedAnimFiles", "Seen in various legion models", "PMARKER", 0x2000),
("16384","Unknown", "", "QUESTION", 0x4000),
("32768","Unknown", "Seen in UI_MainMenu_Legion", "QUESTION", 0x8000),
("65536","Unknown", "", "QUESTION", 0x10000),
("131072","Unknown", "", "QUESTION", 0x20000),
("262144","Unknown", "", "QUESTION", 0x40000),
("524288","Unknown", "", "QUESTION", 0x80000),
("1048576","Unknown", "", "QUESTION", 0x100000),
("2097152","Unknown", "apparently: use 24500 upgraded model format: chunked .anim files, change in the exporter reordering sequence+bone blocks before name", "QUESTION", 0x200000),
]
VERTEX_SHADERS = [
("0", "Diffuse_T1", ""),
("1", "Diffuse_Env", ""),
("2", "Diffuse_T1_T2", ""),
("3", "Diffuse_T1_Env", ""),
("4", "Diffuse_Env_T1", ""),
("5", "Diffuse_Env_Env", ""),
("6", "Diffuse_T1_Env_T1", ""),
("7", "Diffuse_T1_T1", ""),
("8", "Diffuse_T1_T1_T1", ""),
("9", "Diffuse_EdgeFade_T1", ""),
("10", "Diffuse_T2", ""),
("11", "Diffuse_T1_Env_T2", ""),
("12", "Diffuse_EdgeFade_T1_T2", ""),
("13", "Diffuse_EdgeFade_Env", ""),
("14", "Diffuse_T1_T2_T1", ""),
("15", "Diffuse_T1_T2_T3", ""),
("16", "Color_T1_T2_T3", ""),
("17", "BW_Diffuse_T1", ""),
("18", "BW_Diffuse_T1_T2", "")
]
FRAGMENT_SHADERS = [
("0", "Combiners_Opaque", ""),
("1", "Combiners_Mod", ""),
("2", "Combiners_Opaque_Mod", ""),
("3", "Combiners_Opaque_Mod2x", ""),
("4", "Combiners_Opaque_Mod2xNA", ""),
("5", "Combiners_Opaque_Opaque", ""),
("6", "Combiners_Mod_Mod", ""),
("7", "Combiners_Mod_Mod2x", ""),
("8", "Combiners_Mod_Add", ""),
("9", "Combiners_Mod_Mod2xNA", ""),
("10", "Combiners_Mod_AddNA", ""),
("11", "Combiners_Mod_Opaque", ""),
("12", "Combiners_Opaque_Mod2xNA_Alpha", ""),
("13", "Combiners_Opaque_AddAlpha", ""),
("14", "Combiners_Opaque_AddAlpha_Alpha", ""),
("15", "Combiners_Opaque_Mod2xNA_Alpha_Add", ""),
("16", "Combiners_Mod_AddAlpha", ""),
("17", "Combiners_Mod_AddAlpha_Alpha", ""),
("18", "Combiners_Opaque_Alpha_Alpha", ""),
("19", "Combiners_Opaque_Mod2xNA_Alpha_3s", ""),
("20", "Combiners_Opaque_AddAlpha_Wgt", ""),
("21", "Combiners_Mod_Add_Alpha", ""),
("22", "Combiners_Opaque_ModNA_Alpha", ""),
("23", "Combiners_Mod_AddAlpha_Wgt", ""),
("24", "Combiners_Opaque_Mod_Add_Wgt", ""),
("25", "Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha", ""),
("26", "Combiners_Mod_Dual_Crossfade", ""),
("27", "Combiners_Opaque_Mod2xNA_Alpha_Alpha", ""),
("28", "Combiners_Mod_Masked_Dual_Crossfade", ""),
("29", "Combiners_Opaque_Alpha", ""),
("30", "Guild", ""),
("31", "Guild_NoBorder", ""),
("32", "Guild_Opaque", ""),
("33", "Combiners_Mod_Depth", ""),
("34", "Illum", ""),
("35", "Combiners_Mod_Mod_Mod_Const", "")
]
SHADERS = [
('0', 'Combiners_Opaque_Mod2xNA_Alpha_Diffuse_T1_Env', ''),
('1', 'Combiners_Opaque_AddAlpha_Diffuse_T1_Env', ''),
('2', 'Combiners_Opaque_AddAlpha_Alpha_Diffuse_T1_Env', ''),
('3', 'Combiners_Opaque_Mod2xNA_Alpha_Add_Diffuse_T1_Env_T1', ''),
('4', 'Combiners_Mod_AddAlpha_Diffuse_T1_Env', ''),
('5', 'Combiners_Opaque_AddAlpha_Diffuse_T1_T1', ''),
('6', 'Combiners_Mod_AddAlpha_Diffuse_T1_T1', ''),
('7', 'Combiners_Mod_AddAlpha_Alpha_Diffuse_T1_Env', ''),
('8', 'Combiners_Opaque_Alpha_Alpha_Diffuse_T1_Env', ''),
('9', 'Combiners_Opaque_Mod2xNA_Alpha_3s_Diffuse_T1_Env_T1', ''),
('10', 'Combiners_Opaque_AddAlpha_Wgt_Diffuse_T1_T1', ''),
('11', 'Combiners_Mod_Add_Alpha_Diffuse_T1_Env', ''),
('12', 'Combiners_Opaque_ModNA_Alpha_Diffuse_T1_Env', ''),
('13', 'Combiners_Mod_AddAlpha_Wgt_Diffuse_T1_Env', ''),
('14', 'Combiners_Mod_AddAlpha_Wgt_Diffuse_T1_T1', ''),
('15', 'Combiners_Opaque_AddAlpha_Wgt_Diffuse_T1_T2', ''),
('16', 'Combiners_Opaque_Mod_Add_Wgt_Diffuse_T1_Env', ''),
('17', 'Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha1', ''),
('18', 'Combiners_Mod_Dual_Crossfade_Diffuse_T1', ''),
('19', 'Combiners_Mod_Depth_Diffuse_EdgeFade_T1', ''),
('20', 'Combiners_Opaque_Mod2xNA_Alpha_Alpha_Diffuse_T1_Env_T2', ''),
('21', 'Combiners_Mod_Mod_Diffuse_EdgeFade_T1_T2', ''),
('22', 'Combiners_Mod_Masked_Dual_Crossfade_Diffuse_T1_T2', ''),
('23', 'Combiners_Opaque_Alpha_Diffuse_T1_T1', ''),
('24', 'Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha2', ''),
('25', 'Combiners_Mod_Depth_Diffuse_EdgeFade_Env', ''),
('26', 'Guild_Diffuse_T1_T2_T1', ''),
('27', 'Guild_NoBorder_Diffuse_T1_T2', ''),
('28', 'Guild_Opaque_Diffuse_T1_T2_T1', ''),
('29', 'Illum_Diffuse_T1_T1', ''),
('30', 'Combiners_Mod_Mod_Mod_Const_Diffuse_T1_T2_T3', ''),
('31', 'Combiners_Mod_Mod_Mod_Const_Color_T1_T2_T3', ''),
('32', 'Combiners_Opaque_Diffuse_T1', ''),
('33', 'Combiners_Mod_Mod2x_Diffuse_EdgeFade_T1_T2', ''),
]
TEX_UNIT_FLAGS = [
("1", "Invert", "", 'MOD_DATA_TRANSFER', 0x1),
("2", "Transform", "", 'SCULPTMODE_HLT', 0x2),
("4", "Projected Texture", "", 'MOD_UVPROJECT', 0x4),
("8", "Unknown", "", 'QUESTION', 0x8),
("16", "Batch Compatible", "", 'SETTINGS', 0x10),
("32", "Projected Texture 2", "", 'MOD_UVPROJECT', 0x20),
("64", "Use Texture Weights", "", 'WPAINT_HLT', 0x40),
("128", "Unknown", "", 'QUESTION', 0x80),
]
RENDER_FLAGS = [
("1", "Unlit", "Disable lighting", 'SNAP_VOLUME', 0x1),
("2", "Unfogged", "Disable fog", 'MOD_FLUID', 0x2),
("4", "Two-sided", "Render from both sides", 'MOD_UVPROJECT', 0x4),
("8", "Depth-Test", "Unknown", 'SPACE3', 0x8),
("16", "Depth-Write", "Unknown", 'SPACE2', 0x10),
]
BLENDING_MODES = [
("0", "Opaque", "Blending disabled", 'MESH_CUBE', 1),
("1", "AlphaKey", "All pixels are fully opaque or transparent, leading to aliasing (“jaggies”)", 'MOD_BEVEL', 2),
("2", "Alpha", "All pixels can support full transparency range. Sometimes thus can produce some rendering issues", 'MOD_CAST', 3),
("3", "NoAlphaAdd", "Takes the pixels of the Material and adds them to the pixels of the background. This means that there is no darkening; since all pixel values are added together, blacks will just render as transparent", 'FORCE_TEXTURE', 4),
("4", "Add", "This Blend Mode works by taking in an Opacity value or texture and applying it to the surface such that black areas are completely transparent, white areas are completely opaque, and the varying shades of gradation between result in corresponding transparency levels", 'TPAINT_HLT', 5),
("5", "Mod", "The Modulate Blend Mode simply multiplies the value of the Material against the pixels of the background", 'FACESEL', 6),
("6", "Mod2X", "Probably is used in particles. Needs to be researched", 'MOD_PARTICLES', 7),
("7", "BlendAdd", "Probably is used in particles. Needs to be researched", 'MOD_PARTICLES', 8)
]
TEXTURE_TYPES = [
("0", "Hardcoded", "Texture given in filename", 'PMARKER', 1),
("1", "Skin", "Body and clothes", 'PMARKER', 2),
("2", "Object Skin", "Items, Capes", 'PMARKER', 3),
("3", "Weapon Blade", "Armor reflect", 'PMARKER', 4),
("4", "Weapon Handle", "Weapon Handle", 'PMARKER', 5),
("5", "Environment", "Environment (OBSOLETE)", 'PMARKER', 5),
("6", "Hair", "Character hair", 'PMARKER', 7),
("7", "Facial Hair", "Character facial hair", 'PMARKER', 8),
("8", "Skin Extra", "Skin Extra", 'PMARKER', 9),
("9", "UI Skin", "UI Skin (inventory models)", 'PMARKER', 10),
("10", "Tauren Mane", "Tauren Mane (OBSOLETE)", 'PMARKER', 11),
("11", "Monster 1", "Monster Skin 1", 'PMARKER', 12),
("12", "Monster 2", "Monster Skin 2", 'PMARKER', 13),
("13", "Monster 3", "Monster Skin 3", 'PMARKER', 14),
("14", "Item Icon", "Item icon", 'PMARKER', 15),
("15", "Guild Background Color", "", 'PMARKER', 16),
("16", "Guild Emblem Color", "", 'PMARKER', 17),
("17", "Guild Border Color", "", 'PMARKER', 18),
("18", "Guild Emblem", "", 'PMARKER', 19),
("19", "Eyes", "", 'PMARKER', 20),
("20", "Accessory", "", 'PMARKER', 21),
("21", "Secondary Skin", "", 'PMARKER', 22),
("22", "Secondary Hair", "", 'PMARKER', 23),
("23", "Unknown: 23", "", 'PMARKER', 24),
("24", "Unknown: 24", "", 'PMARKER', 25)
]
def get_texture_type_name(texture_type_id):
for field in TEXTURE_TYPES:
if int(field[0]) == texture_type_id:
return "DBC {}".format(field[1])
return "DBC texture type {}".format(str(texture_type_id))
TEXTURE_FLAGS = [
("1", "Wrap X", "Texture wrap X", 'TRIA_RIGHT', 0x1),
("2", "Wrap Y", "Texture wrap Y", 'TRIA_UP', 0x2)
]
TEXTURE_MAPPING = [
("UVMap", "First UVMap", "Use the first UVMap"),
("UVMap.001", "Second UVMap", "Use the second UVMap"),
("Env", "Environmental Mapping", "Use environmental mapping"),
]
BONE_FLAGS = [
("1", "Ignore Parent Translate", "", 'PMARKER', 0x1),
("2", "Ignore Parent Scale", "", 'PMARKER', 0x2),
("4", "Ignore Parent Rotation", "", 'PMARKER', 0x4),
("8", "Spherical Billboard", "", 'PMARKER', 0x8),
("16", "Cylindrical Billboard Lock X", "", 'PMARKER', 0x10),
("32", "Cylindrical Billboard Lock Y", "", 'PMARKER', 0x20),
("64", "Cylindrical Billboard Lock Z", "", 'PMARKER', 0x40),
("512", "Transformed", "", 'PMARKER', 0x200),
("1024", "Kinematic Bone", "MoP+. Allow physics to influence this bone", 'PMARKER', 0x400),
("4096", "Helmet Anim Scaled", "", 'PMARKER', 0x1000),
]
MESH_PART_TYPES = [
("Skin", "Skin", "Character body geoset", 'PMARKER', 1),
("Hair", "Hair", "Character hair geosets", 'PMARKER', 2),
("Facial1", "Facial1", "Usually beard geosets", 'PMARKER', 3),
("Facial2", "Facial2", "Usually mustache geosets", 'PMARKER', 4),
("Facial3", "Facial3", "Usually sideburns geosets", 'PMARKER', 5),
("Glove", "Glove", "Glove geosets", 'PMARKER', 6),
("Boots", "Boots", "Boots geosets", 'PMARKER', 7),
("Shirt", "Shirt", "", 'PMARKER', 8),
("Ears", "Ears", "Ears geosets", 'PMARKER', 9),
("Wristbands", "Wristbands", "Wristbands / Sleeves geosets", 'PMARKER', 10),
("Kneepads", "Kneepads", "Kneepad geosets", 'PMARKER', 11),
("Chest", "Chest", "Chest geosets", 'PMARKER', 12),
("Pants", "Pants", "Pants geosets", 'PMARKER', 13),
("Tabard", "Tabard", "Tabard geosets", 'PMARKER', 14),
("Legs", "Trousers", "Trousers geosets", 'PMARKER', 15),
("ShirtDoublet", "Loincloth", "", 'PMARKER', 16),
("Cape", "Cape", "Cape geosets", 'PMARKER', 17),
("FacialJewelry", "FacialJewelry", "", 'PMARKER', 18),
("EyeEffects", "EyeEffects", "EyeEffects geosets", 'PMARKER', 19),
("Belt", "Belt", "Belt / Bellypack geosets", 'PMARKER', 20),
("Trail", "Trail", "Trail geosets / Undead bones (Legion+)", 'PMARKER', 21),
("Feet", "Feet", "Feet geosets", 'PMARKER', 22),
("Hands", "BE Hands", "Hands for Blood Elf / Night Elf (Legion+)", 'PMARKER', 23),
("Horns", "Horns", "Horns for Draenei/Tauren", 'PMARKER', 24),
("Head", "Head", "", 'PMARKER', 25),
("Torso", "Torso", "", 'PMARKER', 26),
("Shoulders", "Shoulders", "", 'PMARKER', 27),
("Helmet", "Helmet", "", 'PMARKER', 28),
("ArmUpper", "ArmUpper", "", 'PMARKER', 29),
("ArmsReplace", "ArmsReplace", "Mechagnome arms/hands, BFA+", 'PMARKER', 30),
("LegsReplace", "LegsReplace", "Mechagnome legs, BFA+", 'PMARKER', 31),
("FeetReplace", "FeetReplace", "Mechagnome feet, BFA+", 'PMARKER', 32),
("HeadSwap", "HeadSwap", "SL+", 'PMARKER', 33),
("Eyes", "Eyes", "SL+", 'PMARKER', 34),
("Eyebrows", "Eyebrows", "SL+", 'PMARKER', 35),
("Piercings", "Piercings/Earrings", "SL+", 'PMARKER', 36),
("Necklaces", "Necklaces", "SL+", 'PMARKER', 37),
("Headdress", "Headdress", "SL+", 'PMARKER', 38),
("Tail", "Tail", "Draenei SL+", 'PMARKER', 39),
("MiscAccessory", "MiscAccessory", "Vines NE SL+", 'PMARKER', 40),
("MiscFeature", "MiscFeature", "Vines NE SL+", 'PMARKER', 41),
("Noses", "Noses", "Noses Goblins SL+", 'PMARKER', 42),
("HairDecoration", "HairDecoration", "Light Forged Draenei SL+", 'PMARKER', 43),
("HornDecoration", "HornDecoration", "Highmountain Tauren SL+", 'PMARKER', 44),
("BodySize", "BodySize", "Dracthyr DF+", 'PMARKER', 45),
("Unknown1", "Unknown1", "Unknown1 DF+", 'PMARKER', 46),
("Unknown2", "Unknown2", "Unknown2 DF+", 'PMARKER', 47),
("Unknown3", "Unknown3", "Unknown3 DF+", 'PMARKER', 48),
("Unknown4", "Unknown4", "Unknown4 DF+", 'PMARKER', 49),
("Unknown5", "Unknown5", "Unknown5 DF+", 'PMARKER', 50),
("Unknown6", "Unknown6", "Unknown6 DF+", 'PMARKER', 51),
("EyeGlows", "EyeGlows", "EyeGlow (AllRaces) DF+", 'PMARKER', 52),
]
ANIMATION_FLAGS = [
("1", "Init Blend", "Sets Blended flag on M2 init", 'PMARKER', 0x1),
("2", "Unknown", "", 'QUESTION', 0x2),
("4", "Unknown", "", 'QUESTION', 0x4),
("8", "Unknown", "", 'QUESTION', 0x8),
("16", "Unknown", "apparently set during runtime in CM2Shared::LoadLowPrioritySequence for all entries of a loaded sequence (including aliases)", 'QUESTION', 0x10),
("32", "Primary Sequence", "If set, the animation data is in the .m2 file, else in an .anim file", 'MOD_WIREFRAME', 0x20),
("64", "Is Alias", "To find the animation data, the client skips these by following aliasNext until an animation without 0x40 is found.", 'TRIA_RIGHT', 0x40),
("128", "Blended animation", "", 'TRIA_RIGHT', 0x80),
("256", "Unknown", "Sequence stored in model?", 'QUESTION', 0x100),
("512", "Unknown", "", 'QUESTION', 0x200),
("1024", "Unknown", "", 'QUESTION', 0x400),
("2048", "Unknown", "Seen in Legion 24500 models", 'QUESTION', 0x800)
]
PARTICLE_FLAGS = [
("1","Affected By Lighting", "Particles are affected by lighting", "PMARKER", 0x1),
("2","Unknown", "", "QUESTION", 0x2),
("4","Use Player Orientation", "On emission, particle orientation is affected by player orientation", "PMARKER", 0x4),
("8","World Space Up", "Particles travel \"up\" in world space, rather than model space", "PMARKER", 0x8),
("16","Do not trail", "", "PMARKER", 0x10),
("32","Unlightning", "", "PMARKER", 0x20),
("64","Use Burst Multiplier", "", "PMARKER", 0x40),
("128","Use Model Space", "Causes animation of the particle emitter to be carried over to the particles", "PMARKER", 0x80),
("256","Unknown", "", "QUESTION", 0x100),
("512","Random Spawn Position", "Spawn position randomized in some way", "PMARKER", 0x200),
("1024","Pinned Particles", "Particle quad enlarges from their creation position to where they expand", "PMARKER", 0x400),
("2048","Unknown", "", "QUESTION", 0x800),
("4096","XYQuad Particles", "Particles align to XY axis facing Z axis (causes particle to be a tail that orients to the XY grid)", "PMARKER", 0x1000),
("8192","Clamp To Ground", "", "PMARKER", 0x2000),
("16384","Unknown", "", "QUESTION", 0x4000),
("32768","Unknown", "", "QUESTION", 0x8000),
("65536","Random Texture", "", "PMARKER", 0x10000),
("131072","Outwards", "Particles move away from the origin", "PMARKER", 0x20000),
("262144","Inwards", "Particles move toward the origin (unclear flag, sometimes used together with 'Outward')", "QUESTION", 0x40000),
("524288","Independent Scaling", "If set, Scale Vary affects x and y independently. If not set, Scale Vary X is used for both x and y, and Scale Vary Y is not used", "PMARKER", 0x80000),
]
PARTICLE_BLEND_MODES = [
("1","Unknown","","QUESTION",0x1),
("2","Unknown","","QUESTION",0x2),
("4","Unknown","","QUESTION",0x4),
("5","Unknown","","QUESTION",0x5),
]
def generate_enumerated_list(irange, name):
return list([(str(i), "{}_{}".format(i, name), "") for i in irange])
def mesh_part_id_menu(self, context):
geoset_group = self.mesh_part_group
if geoset_group == 'Skin':
return [('0', 'No subtype', "")]
elif geoset_group == 'Hair':
return generate_enumerated_list(M2SkinMeshPartID.Hair.value, 'Hair')
elif geoset_group == 'Facial1':
return generate_enumerated_list(M2SkinMeshPartID.Facial1.value, 'Facial1')
elif geoset_group == 'Facial2':
return generate_enumerated_list(M2SkinMeshPartID.Facial2.value, 'Facial2')
elif geoset_group == 'Facial3':
return generate_enumerated_list(M2SkinMeshPartID.Facial3.value, 'Facial3')
elif geoset_group == 'Glove':
return [("401", "401 - Skin", ""),
("402", "402 - Regular", ""),
("403", "403 - Jackgloves", ""),
("404", "404 - Armored", ""),
("405", "405 - Armored", "")]
elif geoset_group == 'Boots':
return [("501", "501 - Skin", ""),
("502", "502 - Short", ""),
("503", "503 - Jackboots", ""),
("504", "504 - Regular", ""),
("505", "505 - Plate", ""),
("506", "506 - Boots6", ""),
("507", "507 - Boots7", ""),
("508", "508 - Boots8", ""),
("509", "509 - Boots9", ""),
("510", "510 - Boots10", "")]
elif geoset_group == 'Shirt':
return generate_enumerated_list(M2SkinMeshPartID.Shirt.value, 'Shirt')
elif geoset_group == 'Ears':
return [("701", "701 - None (DNE)", "No ears"),
("702", "702 - Ears", "Ears geoset"),
("703", "703 - Ears3", "Ears3"),
("704", "704 - Ears4", "Ears4"),
("705", "705 - Ears5", "Ears5"),
("706", "706 - Ears6", "Ears6"),
("707", "707 - Ears7", "Ears7"),
("708", "708 - Ears8", "Ears8"),
("709", "709 - Ears9", "Ears9"),
("710", "710 - Ears10", "Ears10"),
("711", "711 - Ears11", "Ears11"),
("712", "712 - Ears12", "Ears12")]
elif geoset_group == 'Wristbands':
return [("801", "801 - None (DNE)", "No wristbands"),
("802", "802 - Normal", "Normal wristbands"),
("803", "803 - Ruffled", "Ruffled wristbands"),
("804", "804 - Panda Collar Shirt", "")]
elif geoset_group == 'Kneepads':
return [("901", "901 - None (DNE)", "No kneepads"),
("902", "902 - Long", "Long kneepads"),
("903", "903 - Short", "Short kneepads"),
("904", "904 - Panda Pants", ""),
("905", "905 - Kneepads5", "")]
elif geoset_group == 'Chest':
return [("1001", "1001 - None (DNE)", "No chest"),
("1002", "1002 - Plate", "Downside of a plate chest"),
("1003", "1003 - Body 2", ""),
("1004", "1004 - Body 3", "")]
elif geoset_group == 'Pants':
return [("1101", "1101 - Regular", "Regular pants"),
("1102", "1102 - Skirt", "Short skirt"),
("1104", "1104 - Armored", "Armored pants"),
("1105", "1105 - Regular5", "Pants5")]
elif geoset_group == 'Tabard':
return [("1201", "1201 - None (DNE)", "No tabard"),
("1202", "1202 - Tabard", "Tabard"),
("1203", "1203 - Tabard Unk", "SL +"),
("1204", "1204 - Tabard4", "Tabard4")]
elif geoset_group == 'Legs':
return [("1301", "1301 - Trousers", ""),
("1302", "1302 - Dress", ""),
("1303", "1303 - Legs3", ""),
("1304", "1304 - Legs4", "")]
elif geoset_group == 'ShirtDoublet':
return generate_enumerated_list(M2SkinMeshPartID.ShirtDoublet.value, 'ShirtDoublet')
elif geoset_group == 'Cape':
return [("1501", "1501 - Scarf", "Shortest cloak"),
("1502", "1502 - Knight", "Usually the longest cloak"),
("1503", "1503 - Normal", ""),
("1504", "1504 - Double-tail", ""),
("1505", "1505 - Small", ""),
("1506", "1506 - Small double-tail", ""),
("1507", "1507 - Guild cloak", ""),
("1508", "1508 - Split", "Long"),
("1509", "1509 - Tapered", "Long"),
("1510", "1510 - Notched", "Long"),
("1511", "1511 - Unknown1", "SL+"),
("1512", "1512 - Unknown2", "SL+"),
("1513", "1513 - Unknown3", "SL+"),
("1514", "1514 - Unknown4", "SL+"),
("1515", "1515 - Unknown5", "SL+"),
("1516", "1516 - Unknown6", "SL+"),
("1517", "1517 - Unknown7", "SL+"),
("1518", "1518 - Unknown8", "SL+"),
("1519", "1519 - Unknown9", "SL+"),
("1520", "1520 - Unknown10", "SL+"),
("1521", "1521 - Unknown11", "SL+"),
("1522", "1522 - Unknown12", "SL+"),
("1523", "1523 - Unknown13", "SL+"),
("1524", "1524 - Unknown14", "SL+"),
("1525", "1525 - Unknown15", "SL+")]
elif geoset_group == 'FacialJewelry':
return generate_enumerated_list(M2SkinMeshPartID.FacialJewelry.value, 'FacialJewelry')
elif geoset_group == 'EyeEffects':
return [("1701", "1701 - None (DNE)", "No eyeglow"),
("1702", "1702 - Racial", "Racial eyeglow"),
("1703", "1703 - DK", "Death Knight eyeglow"),
("1704", "1704 - Eyeffects4", "Eyeffects4"),
("1705", "1705 - Eyeffects5", "Eyeffects5")]
elif geoset_group == 'Belt':
return [("1801", "1801 - None (DNE)", "No belt / bellypack"),
("1802", "1802 - Bulky", "Bulky belt"),
("1803", "1803 - Panda Cord Belt", ""),
("1804", "1804 - Belt4", "")]
elif geoset_group == 'Trail':
return generate_enumerated_list(M2SkinMeshPartID.Trail.value, 'Trail')
elif geoset_group == 'Feet':
return [("2001", "2001 - Basic shoes", ""),
("2002", "2002 - Toes", ""),
("2003", "2003 - Feet3", ""),
("2004", "2004 - Feet4", ""),
("2005", "2005 - Feet5", ""),
("2006", "2006 - Feet6", ""),
("2007", "2007 - Feet7", ""),
("2008", "2008 - Feet8", "")]
elif geoset_group == 'Head':
return [("2101", "2101 - Show head", "")]
elif geoset_group == 'Torso':
return [("2201", "2201 - Default", ""),
("2202", "2202 - Covered torso", "")]
elif geoset_group == 'Hands':
return [("2301", "2301 - BE / NE Hands", 'Hands for Blood Elf / Night Elf')]
elif geoset_group == 'Horns':
return generate_enumerated_list(M2SkinMeshPartID.Horns.value, 'Horns')
elif geoset_group == 'Shoulders':
return generate_enumerated_list(M2SkinMeshPartID.Shoulders.value, 'Shoulders')
elif geoset_group == 'Helmet':
return generate_enumerated_list(M2SkinMeshPartID.Helmet.value, 'Helmet')
elif geoset_group == 'ArmUpper':
return generate_enumerated_list(M2SkinMeshPartID.ArmUpper.value, 'ArmUpper')
elif geoset_group == 'ArmsReplace':
return generate_enumerated_list(M2SkinMeshPartID.ArmsReplace.value, 'ArmsReplace')
elif geoset_group == 'LegsReplace':
return generate_enumerated_list(M2SkinMeshPartID.LegsReplace.value, 'LegsReplace')
elif geoset_group == 'FeetReplace':
return generate_enumerated_list(M2SkinMeshPartID.FeetReplace.value, 'FeetReplace')
elif geoset_group == 'HeadSwap':
return generate_enumerated_list(M2SkinMeshPartID.HeadSwap.value, 'HeadSwap')
elif geoset_group == 'Eyes':
return generate_enumerated_list(M2SkinMeshPartID.Eyes.value, 'Eyes')
elif geoset_group == 'Eyebrows':
return generate_enumerated_list(M2SkinMeshPartID.Eyebrows.value, 'Eyebrows')
elif geoset_group == 'Piercings':
return generate_enumerated_list(M2SkinMeshPartID.Piercings.value, 'Piercings')
elif geoset_group == 'Necklaces':
return generate_enumerated_list(M2SkinMeshPartID.Necklaces.value, 'Necklaces')
elif geoset_group == 'Headdress':
return generate_enumerated_list(M2SkinMeshPartID.Headdress.value, 'Headdress')
elif geoset_group == 'Tail':
return generate_enumerated_list(M2SkinMeshPartID.Tail.value, 'Tail')
elif geoset_group == 'MiscAccessory':
return generate_enumerated_list(M2SkinMeshPartID.MiscAccessory.value, 'MiscAccessory')
elif geoset_group == 'MiscFeature':
return generate_enumerated_list(M2SkinMeshPartID.MiscFeature.value, 'MiscFeature')
elif geoset_group == 'Noses':
return generate_enumerated_list(M2SkinMeshPartID.Noses.value, 'Noses')
elif geoset_group == 'HairDecoration':
return generate_enumerated_list(M2SkinMeshPartID.HairDecoration.value, 'HairDecoration')
elif geoset_group == 'HornDecoration':
return generate_enumerated_list(M2SkinMeshPartID.HornDecoration.value, 'HornDecoration')
elif geoset_group == 'BodySize':
return generate_enumerated_list(M2SkinMeshPartID.BodySize.value, 'BodySize')
elif geoset_group == 'Unknown1':
return generate_enumerated_list(M2SkinMeshPartID.Unknown1.value, 'Unknown1')
elif geoset_group == 'Unknown2':
return generate_enumerated_list(M2SkinMeshPartID.Unknown2.value, 'Unknown2')
elif geoset_group == 'Unknown3':
return generate_enumerated_list(M2SkinMeshPartID.Unknown3.value, 'Unknown3')
elif geoset_group == 'Unknown4':
return generate_enumerated_list(M2SkinMeshPartID.Unknown4.value, 'Unknown4')
elif geoset_group == 'Unknown5':
return generate_enumerated_list(M2SkinMeshPartID.Unknown5.value, 'Unknown5')
elif geoset_group == 'Unknown6':
return generate_enumerated_list(M2SkinMeshPartID.Unknown6.value, 'Unknown6')
elif geoset_group == 'EyeGlows':
return generate_enumerated_list(M2SkinMeshPartID.EyeGlows.value, 'EyeGlows')
else:
return [('0', 'No subtype', "")]
def get_keybone_ids(self, context):
keybone_ids = [('-1', 'Not a keybone', '')]
keybone_ids.extend([(str(field.value), field.name, '') for field in M2KeyBones])
return keybone_ids
def get_anim_ids(self, context):
return [(str(seq_id), name, '') for seq_id, name in M2SequenceNames().items()]
def get_attachment_types(self, context):
return [(str(field.value), field.name, "") for field in M2AttachmentTypes]
def get_particle_flags(self, context):
return [(str(field.value), field.name, "", hex(field.value)) for field in M2ParticleFlags]
def get_event_names(self, context):
return [(field.value, field.name, "") for field in M2EventTokens]
@@ -0,0 +1,203 @@
import bpy
import bmesh
from bpy.app.handlers import persistent
from .drivers import register as register_m2_driver_utils
from ...utils.misc import show_message_box, singleton
__reload_order_index__ = 0
@singleton
class DepsgraphLock:
DEPSGRAPH_UPDATE_LOCK = False
def __enter__(self):
self.DEPSGRAPH_UPDATE_LOCK = True
def __exit__(self, exc_type, exc_val, exc_tb):
self.DEPSGRAPH_UPDATE_LOCK = False
_obj_props = (
('wow_m2_geoset', 'geosets'),
('wow_m2_attachment', 'attachments'),
('wow_m2_event', 'events'),
('wow_m2_light', 'lights')
)
def _remove_col_items(scene, col_name):
col = getattr(scene.wow_m2_root_elements, col_name)
for i, obj in enumerate(col):
if obj.pointer and obj.pointer.name not in scene.objects:
col.remove(i)
break
else:
return
_remove_col_items(scene, col_name)
@persistent
def live_update_materials(dummy):
try:
anim = bpy.context.scene.wow_m2_animations[bpy.context.scene.wow_m2_cur_anim_index]
if anim.live_update:
for mat in bpy.data.materials:
if mat.wow_m2_material.live_update:
mat.invert_z = mat.invert_z
except IndexError:
pass
def _add_col_items(scene):
for i, obj in enumerate(scene.objects):
if obj.parent:
for prop, col_name in _obj_props:
if prop == 'wow_m2_light' and obj.type != 'LIGHT':
continue
prop_group = getattr(obj, prop) if prop != 'wow_m2_light' else obj.data.wow_m2_light
if prop_group.enabled:
col = getattr(scene.wow_m2_root_elements, col_name)
if col.find(obj.name) < 0:
slot = col.add()
slot.pointer = obj
@persistent
def load_handler(dummy):
register_m2_driver_utils()
@persistent
def on_depsgraph_update(_):
if DepsgraphLock().DEPSGRAPH_UPDATE_LOCK:
return
delete = False
for update in bpy.context.view_layer.depsgraph.updates:
try:
if isinstance(update.id, bpy.types.Object) and update.id.type == 'MESH':
if update.id.wow_m2_geoset.enabled:
obj = bpy.data.objects[update.id.name, update.id.library]
mesh = obj.data
if obj.mode == 'EDIT':
bm = bmesh.from_edit_mesh(mesh)
if bm.faces.active:
root_elements = bpy.context.scene.wow_m2_root_elements
mat_index_active = bm.faces.active.material_index
if mesh.materials:
mat_index = root_elements.materials.find(mesh.materials[mat_index_active].name)
if mat_index >= 0 and root_elements.cur_material != mat_index:
with DepsgraphLock():
root_elements.cur_material = mat_index
if update.is_updated_geometry:
group_entry = bpy.context.scene.wow_m2_root_elements.geosets.get(obj.name)
if group_entry: # TODO: find out why there is a possible m2 group not in the list yet.
group_entry.export = True
elif update.id.wow_m2_attachment.enabled:
obj = bpy.data.objects[update.id.name, update.id.library]
with DepsgraphLock():
# enforce object mode
if obj.mode != 'OBJECT':
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='OBJECT')
elif isinstance(update.id, bpy.types.Scene):
if bpy.context.view_layer.objects.active \
and bpy.context.view_layer.objects.active.select_get():
# sync collection active items
act_obj = bpy.context.view_layer.objects.active
root_comps = bpy.context.scene.wow_m2_root_elements
if act_obj:
if act_obj.type == 'MESH':
if act_obj.wow_m2_geoset.enabled:
slot_idx = root_comps.geosets.find(act_obj.name)
root_comps.cur_geoset = slot_idx
elif act_obj.wow_m2_attachment.enabled:
slot_idx = root_comps.wow_m2_attachments.find(act_obj.name)
root_comps.cur_attachment = slot_idx
elif act_obj.wow_m2_event.enabled:
slot_idx = root_comps.wow_m2_events.find(act_obj.name)
root_comps.cur_event = slot_idx
elif act_obj.type == 'LAMP':
if act_obj.wow_m2_light.enabled:
slot_idx = root_comps.lights.find(act_obj.name)
root_comps.cur_light = slot_idx
# fill collections
n_objs = len(bpy.context.scene.objects) -1 # -1 is to ignore base armature
if n_objs == bpy.wbs_n_scene_objects:
continue
if n_objs < bpy.wbs_n_scene_objects:
bpy.wbs_n_scene_objects = n_objs
_remove_col_items(bpy.context.scene, 'geosets')
_remove_col_items(bpy.context.scene, 'lights')
_remove_col_items(bpy.context.scene, 'events')
_remove_col_items(bpy.context.scene, 'attachments')
else:
bpy.wbs_n_scene_objects = n_objs
_add_col_items(bpy.context.scene)
elif isinstance(update.id, bpy.types.Material):
mat = bpy.data.materials[update.id.name, update.id.library]
if mat.wow_m2_material.enabled \
and bpy.context.scene.wow_m2_root_elements.materials.find(mat.name) < 0:
mat.wow_m2_material.enabled = False
slot = bpy.context.scene.wow_m2_root_elements.materials.add()
slot.pointer = mat
elif isinstance(update.id, bpy.types.Image):
image = bpy.data.images[update.id.name, update.id.library]
if image.wow_m2_texture.enabled \
and bpy.context.scene.wow_m2_root_elements.textures.find(image.name) < 0:
image.wow_m2_texture.enabled = False
slot = bpy.context.scene.wow_m2_root_elements.textures.add()
slot.pointer = image
finally:
DepsgraphLock().DEPSGRAPH_UPDATE_LOCK = False
# def register():
# bpy.wbs_n_scene_objects = 0
# bpy.app.handlers.frame_change_pre.append(live_update_materials)
# load_handler(None)
# bpy.app.handlers.load_post.append(load_handler)
# bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
# def unregister():
# bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
# del bpy.wbs_n_scene_objects
# bpy.app.handlers.frame_change_pre.remove(live_update_materials)
# bpy.app.handlers.load_post.remove(load_handler)
@@ -0,0 +1,93 @@
from ensurepip import version
import bpy
from ....pywowlib.enums.m2_enums import M2AttachmentTypes, M2SequenceNames
# from ...ui.enums import get_attachment_types
def update_attachment_type(self, context):
# self.attachment_type.items.append
pass
def set_attachment_types_enum(self, context):
enum = []
attachments = [obj.wow_m2_attachment.type for obj in bpy.data.objects if obj.type == 'EMPTY' and obj.wow_m2_attachment.enabled]
for i, field in enumerate(M2AttachmentTypes):
if i not in attachments:
if context.scene.wow_scene.version == '2' and i <= 46:
enum.append((str(field.value), field.name, ""))
elif context.scene.wow_scene.version == '6':
enum.append((str(field.value), field.name, ""))
return enum
class M2_OT_add_attachment(bpy.types.Operator):
bl_idname = 'scene.m2_add_attachment'
bl_label = 'Add attachment'
bl_description = 'Add a M2 attachment object to the scene'
bl_options = {'REGISTER', 'UNDO'}
attachment_type: bpy.props.EnumProperty(
name="Attachment type",
description="Select M2 component entity objects",
# items=get_attachment_types,
items=set_attachment_types_enum,
# default='19'
# update=update_attachment_type
)
def execute(self, context):
scn = bpy.context.scene
bpy.ops.object.empty_add(type='SPHERE', location=(0, 0, 0))
obj = bpy.context.view_layer.objects.active
obj.empty_display_size = 0.07
bpy.ops.object.constraint_add(type='CHILD_OF')
constraint = obj.constraints[-1]
rig = list(filter(lambda ob: ob.type == 'ARMATURE' and not ob.hide_get(), bpy.context.scene.objects))[0]
bpy.context.view_layer.objects.active = rig
bpy.ops.object.mode_set(mode='EDIT')
armature = rig.data
constraint.target = rig
obj.parent = rig
# TODO : find or create matching animation bone ?
if len(armature.edit_bones) > 0:
bone = armature.edit_bones[0]
constraint.subtarget = bone.name
bpy.ops.object.mode_set(mode='OBJECT')
attachment_id = self.attachment_type
# obj.location = attachment.position
obj.location = scn.cursor.location
attachments = [obj for obj in bpy.data.objects if obj.type == 'EMPTY' and obj.wow_m2_attachment.enabled]
bpy.context.view_layer.objects.active = obj
# name based on selected attach type
obj.name = M2AttachmentTypes.get_attachment_name(int(attachment_id), len(attachments))
obj.wow_m2_attachment.enabled = True
obj.wow_m2_attachment.type = str(attachment_id)
# animate attachment
obj.animation_data_create()
obj.animation_data.action_blend_type = 'ADD'
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="Info: Successfully created attachment: " + obj.name + "!", font_size=24, y_offset=67)
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="You might want to edit the bone parent in object's constraint properties.", font_size=24, y_offset=100)
self.report({'INFO'}, "Successfully created M2 attachment: " + obj.name + "\nYou might want to edit the bone parent in object's constraint properties.")
# for attachment in attachments:
# if attachment.wow_m2_attachment.type == attachment_id:
# self.report({'WARNING'}, 'An attachment of this type already exists in the model.')
return {'FINISHED'}
@@ -0,0 +1,85 @@
from ensurepip import version
import bpy
from ....pywowlib.enums.m2_enums import M2EventTokens, M2SequenceNames
def update_event_type(self, context):
# self.attachment_type.items.append
pass
def set_event_types_enum(self, context):
enum = []
events = [obj.wow_m2_event.token for obj in bpy.data.objects if obj.type == 'EMPTY' and obj.wow_m2_event.enabled]
for field in M2EventTokens:
# TODO : versions
# if version == lichking and field.value > 52 (?): return
enum.append((str(field.value), field.name, ""))
return enum
class M2_OT_add_Event(bpy.types.Operator):
bl_idname = 'scene.m2_add_event'
bl_label = 'Add event'
bl_description = 'Add a M2 event object to the scene'
bl_options = {'REGISTER', 'UNDO'}
event_type: bpy.props.EnumProperty(
name="Event type",
description="Select M2 component entity objects",
items=set_event_types_enum,
)
def execute(self, context):
scn = bpy.context.scene
bpy.ops.object.empty_add(type='SPHERE', location=(0, 0, 0))
obj = bpy.context.view_layer.objects.active
obj.empty_display_size = 0.07
bpy.ops.object.constraint_add(type='CHILD_OF')
constraint = obj.constraints[-1]
rig = list(filter(lambda ob: ob.type == 'ARMATURE' and not ob.hide_get(), bpy.context.scene.objects))[0]
bpy.context.view_layer.objects.active = rig
bpy.ops.object.mode_set(mode='EDIT')
armature = rig.data
constraint.target = rig
obj.parent = rig
# TODO : find or create matching animation bone ?
if len(armature.edit_bones) > 0:
bone = armature.edit_bones[0]
constraint.subtarget = bone.name
bpy.ops.object.mode_set(mode='OBJECT')
event_type = self.event_type
# obj.location = attachment.position
obj.location = scn.cursor.location
events = [obj for obj in bpy.data.objects if obj.type == 'EMPTY' and obj.wow_m2_event.enabled]
bpy.context.view_layer.objects.active = obj
# name based on selected attach type
obj.name = ('Event_' + M2EventTokens.get_event_name(event_type))
obj.wow_m2_event.enabled = True
obj.wow_m2_event.token = str(event_type)
# animate attachment
obj.animation_data_create()
obj.animation_data.action_blend_type = 'ADD'
self.report({'INFO'}, "Successfully created M2 event: " + obj.name + "\nYou might want to edit the bone parent in object's constraint properties.")
return {'FINISHED'}
@@ -0,0 +1,57 @@
import bpy
import os
import traceback
import struct
from ...import_m2 import import_m2_gamedata
from ....wmo.utils.wmv import wmv_get_last_m2, wow_export_get_last_m2, noggit_red_get_last_m2
from ....ui.preferences import get_project_preferences
from ....utils.misc import load_game_data
class M2_OT_import_last_m2_from_wmv(bpy.types.Operator):
bl_idname = "scene.wow_import_last_m2_from_wmv"
bl_label = "Load last M2 from preferred import method"
bl_description = "Load last M2 from preferred import method"
bl_options = {'UNDO', 'REGISTER'}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
game_data = load_game_data()
if not game_data or not game_data.files:
self.report({'ERROR'}, "Failed to import model. Connect to game client first.")
return {'CANCELLED'}
project_preferences = get_project_preferences()
if project_preferences.import_method == 'WMV':
if project_preferences.wmv_path:
m2_path = wmv_get_last_m2()
elif project_preferences.import_method == 'WowExport':
if project_preferences.wow_export_path:
m2_path = wow_export_get_last_m2()
elif project_preferences.import_method == 'NoggitRed':
if project_preferences.noggit_red_path:
m2_path = noggit_red_get_last_m2()
if not m2_path:
self.report({'ERROR'}, """Log contains no M2 entries.
Make sure to use compatible WMV version or WoW.Export and open an .m2 there.""")
return {'CANCELLED'}
try:
import_m2_gamedata(2, m2_path, False)
except:
traceback.print_exc()
self.report({'ERROR'}, "Failed to import model.")
return {'CANCELLED'}
self.report({'INFO'}, "Done importing M2 object to scene.")
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
@@ -0,0 +1,78 @@
import bpy
class M2RenameOperator(bpy.types.Operator):
"""Rename object, all groups and fcurve data_path"""
bl_idname = "object.m2_bone_renamer"
bl_label = "M2 Bone Renamer"
bl_options = {'REGISTER', 'UNDO_GROUPED'}
prefix: bpy.props.StringProperty(
name="Prefix",
description="Prefix add will be added to all objects",
default="default_",
maxlen=1024,
)
rename_armature: bpy.props.BoolProperty(
name="Rename Armature",
description="Should the selected armature be renamed?",
default = False
)
rename_objects: bpy.props.BoolProperty(
name="Rename All Objects",
description="Rename all Scene Objects?",
default = False
)
def draw(self, context):
layout = self.layout
layout.prop(self, "prefix", text='Prefix')
layout.prop(self, "rename_armature", text='Rename Armature')
layout.prop(self, "rename_objects", text='Rename Objects')
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
prefix = self.prefix
rename_armature = self.rename_armature
rename_objects = self.rename_objects
obj = context.object
if obj is None:
self.report({"WARNING"}, "No object selected! Aborting action...")
return {'FINISHED'}
if obj.type != 'ARMATURE':
self.report({"WARNING"}, "Selected object is not a rig (Armature)! Aborting action...")
return {'FINISHED'}
#Rename Armature
if rename_armature and not obj.name.startswith(prefix):
obj.name = prefix + obj.name
#Rename Objects
if rename_objects:
for object in bpy.data.objects:
if object != obj and not object.name.startswith(prefix):
object.name = prefix + object.name
#Rename Bones
if hasattr(obj.data, 'bones') and len(obj.data.bones) > 0:
for bone in obj.data.bones:
if not bone.name.startswith(prefix):
bone.name = prefix + bone.name
#Rename FCurves
for action in bpy.data.actions:
for group in action.groups:
if not group.name.startswith(prefix):
oldname = group.name
group.name = prefix + oldname
for fcurve in group.channels:
fcurve.data_path = fcurve.data_path.replace(oldname, group.name)
return {'FINISHED'}
@@ -0,0 +1,83 @@
import bpy
from ....wmo.utils.materials import load_texture
from ....wmo.utils.wmv import wmv_get_last_texture, wow_export_get_last_texture
from ....ui.preferences import get_project_preferences
from ....utils.misc import load_game_data, resolve_outside_texture_path, resolve_texture_path
class M2_fill_textures(bpy.types.Operator):
bl_idname = 'scene.m2_fill_textures'
bl_label = 'Fill textures'
bl_description = "Fill Textures fields of WoW materials with paths from applied image"
bl_options = {'REGISTER', 'UNDO_GROUPED'}
def execute(self, context):
for ob in bpy.context.selected_objects:
mesh = ob.data
if mesh is None and ob.wow_m2_particle.enabled:
texture = ob.wow_m2_particle.texture
resolved_path = resolve_texture_path(texture.filepath)
if resolved_path is None:
resolved_path = resolve_outside_texture_path(texture.filepath)
texture.wow_m2_texture.path = resolved_path
else:
for material in mesh.materials:
if material.wow_m2_material.texture_1:
texture = material.wow_m2_material.texture_1
resolved_path = resolve_texture_path(texture.filepath)
if resolved_path is None:
resolved_path = resolve_outside_texture_path(texture.filepath)
texture.wow_m2_texture.path = resolved_path
if material.wow_m2_material.texture_2:
texture2 = material.wow_m2_material.texture_2
resolved_path = resolve_texture_path(texture2.filepath)
if resolved_path is None:
resolved_path = resolve_outside_texture_path(texture2.filepath)
texture2.wow_m2_texture.path = resolved_path
self.report({'INFO'}, "Done filling texture paths")
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="Info: Done filling texture paths!", font_size=24, y_offset=67)
return {'FINISHED'}
class M2_OT_import_texture(bpy.types.Operator):
bl_idname = "scene.wow_m2_texture_import"
bl_label = "Import WoW Texture"
bl_description = "Import last texture from Import Method as a M2 material."
bl_options = {'UNDO', 'REGISTER'}
def execute(self, context):
project_preferences = get_project_preferences()
game_data = load_game_data()
if not game_data:
self.report({'ERROR'}, "Importing texture failed. Game data was not loaded.")
return {'CANCELLED'}
if project_preferences.import_method == 'WMV':
path = wmv_get_last_texture().capitalize()
elif project_preferences.import_method == 'WowExport' or 'NoggitRed':
path = wow_export_get_last_texture().capitalize()
if not path:
self.report({'ERROR'}, "Log does not contain any texture paths.")
return {'CANCELLED'}
game_data.extract_textures_as_png(project_preferences.cache_dir_path, (path,))
texture = load_texture({}, path, project_preferences.cache_dir_path)
print("Info: Successfully imported texture: " + texture.name)
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="Info: Successfully imported texture: " + texture.name, font_size=24, y_offset=67)
return {'FINISHED'}
@@ -0,0 +1,53 @@
import bpy
import io
import os
import bmesh
from .... import PACKAGE_NAME
from ....utils.misc import load_game_data
from ....pywowlib.blp import PNG2BLP
# from ....pywowlib.io_utils.types import *
from ....third_party.tqdm import tqdm
class M2_OT_select_entity(bpy.types.Operator):
bl_idname = 'scene.wow_m2_select_entity'
bl_label = 'Select m2 entities'
bl_description = 'Select all M2 entities of given type'
bl_options = {'REGISTER', 'INTERNAL'}
entity: bpy.props.EnumProperty(
name="Entity",
description="Select M2 component entity objects",
items=[
("wow_m2_geoset", "Geosets", ""),
("wow_m2_attachment", "Attachments", ""),
("wow_m2_event", "Events", ""),
("wow_m2_particle", "particles", ""),
("wow_m2_camera", "Cameras", ""),
("wow_m2_light", "Lights", ""),
("Collision", "Collision", ""),
("Skeleton", "Bones", "")
]
)
def execute(self, context):
for obj in bpy.context.scene.objects:
if obj.hide_get():
continue
if obj.type == 'MESH':
if obj.wow_m2_geoset:
obj.select_set(True)
if obj.wow_m2_geoset.collision_mesh:
obj.wow_m2_geoset.select_set(True)
elif self.entity not in ("wow_m2_light", "wow_m2_geoset", "Collision"):
if getattr(obj, self.entity):
obj.select_set(True)
elif obj.type == 'LIGHT' and self.entity == "wow_m2_light":
obj.select_set(True)
return {'FINISHED'}
@@ -0,0 +1,174 @@
import bpy
from ... import util as util
class M2_OT_disable_drivers(bpy.types.Operator):
bl_idname = 'scene.m2_ot_disable_drivers'
bl_label = 'Disable Drivers'
bl_description = "Disables drivers from materials so you can copy/paste them to other scenes"
bl_options = {'REGISTER', 'UNDO_GROUPED'}
def execute(self, context):
for mat in bpy.data.materials:
if not mat.node_tree:
continue
nodes = mat.node_tree.nodes
transparency_node = nodes.get("Transparency")
color_node = nodes.get("Color")
color_alpha_mix = nodes.get("Color_Alpha_Mix")
# Transparency
if transparency_node:
input_socket = transparency_node.inputs[1]
if not input_socket.is_linked:
transp_driver_path = 'nodes["Transparency"].inputs[1].default_value'
transparency_node.label = transparency_node.label.replace("ON", "OFF")
if mat.node_tree.animation_data:
for drv in mat.node_tree.animation_data.drivers:
if drv.data_path == transp_driver_path:
try:
transparency_node.inputs[1].driver_remove("default_value")
except TypeError as e:
print(f"Error removing Transparency driver: {e}")
break
# Color node (RGB)
if color_node:
color_driver_path = 'nodes["Color"].inputs[7].default_value'
color_node.label = color_node.label.replace("ON", "OFF")
if mat.node_tree.animation_data:
for drv in mat.node_tree.animation_data.drivers:
if drv.data_path.startswith(color_driver_path):
try:
color_node.inputs[7].driver_remove("default_value")
except (TypeError, IndexError) as e:
print(f"Error removing Color driver: {e}")
break
# Color Alpha Mix
if color_alpha_mix:
input_socket = color_alpha_mix.inputs[1]
if not input_socket.is_linked:
col_alpha_driver_path = 'nodes["Color_Alpha_Mix"].inputs[1].default_value'
color_alpha_mix.label = color_alpha_mix.label.replace("ON", "OFF")
if mat.node_tree.animation_data:
for drv in mat.node_tree.animation_data.drivers:
if drv.data_path == col_alpha_driver_path:
try:
color_alpha_mix.inputs[1].driver_remove("default_value")
except TypeError as e:
print(f"Error removing Color_Alpha_Mix driver: {e}")
break
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="Info: Drivers Disabled!", font_size=24, y_offset=67)
return {'FINISHED'}
class M2_OT_enable_drivers(bpy.types.Operator):
bl_idname = 'scene.m2_ot_enable_drivers'
bl_label = 'Enable Drivers'
bl_description = "Enables drivers for materials after copying/pasting them to other scenes"
bl_options = {'REGISTER', 'UNDO_GROUPED'}
def execute(self, context):
controller = util.find_color_transparency_controller()
for mat in bpy.data.materials:
if not mat.node_tree:
continue
nodes = mat.node_tree.nodes
transparency_node = nodes.get("Transparency")
color_node = nodes.get("Color")
color_alpha_mix = nodes.get("Color_Alpha_Mix")
# --- Transparency driver ---
if transparency_node:
input_socket = transparency_node.inputs[1]
if not input_socket.is_linked:
driver_path = 'nodes["Transparency"].inputs[1].default_value'
driver_exists = any(
drv.data_path == driver_path
for drv in (mat.node_tree.animation_data.drivers if mat.node_tree.animation_data else [])
)
if not driver_exists:
try:
driver = transparency_node.inputs[1].driver_add("default_value").driver
driver.type = 'SCRIPTED'
driver.expression = 'Transparency'
var = driver.variables.new()
var.name = 'Transparency'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = controller
trans_name = mat.wow_m2_material.transparency
trans_index = int(''.join(filter(str.isdigit, trans_name)))
var.targets[0].data_path = f"wow_m2_color_transparency.transparencies[{trans_index}].value"
transparency_node.label = transparency_node.label.replace("OFF", "ON")
except Exception as e:
print(f"Error adding Transparency driver: {e}")
# --- Color RGB drivers ---
color_name = mat.wow_m2_material.color
color_index = int(''.join(filter(str.isdigit, color_name))) if color_name else 0
if color_node:
color_components = ['R', 'G', 'B']
if color_node.inputs[0].default_value == 1.0:
color_node.label = color_node.label.replace("OFF", "ON")
for i, comp in enumerate(color_components):
driver_path = f'nodes["Color"].inputs[7].default_value[{i}]'
driver_exists = any(
drv.data_path == driver_path
for drv in (mat.node_tree.animation_data.drivers if mat.node_tree.animation_data else [])
)
if not driver_exists:
try:
driver = color_node.inputs[7].driver_add("default_value", i).driver
driver.type = 'SCRIPTED'
driver.expression = comp
var = driver.variables.new()
var.name = comp
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = controller
var.targets[0].data_path = f"wow_m2_color_transparency.colors[{color_index}].color[{i}]"
except Exception as e:
print(f"Error adding Color driver ({comp}): {e}")
# --- Color Alpha driver ---
if color_alpha_mix:
input_socket = color_alpha_mix.inputs[1]
if not input_socket.is_linked:
driver_path = 'nodes["Color_Alpha_Mix"].inputs[1].default_value'
driver_exists = any(
drv.data_path == driver_path
for drv in (mat.node_tree.animation_data.drivers if mat.node_tree.animation_data else [])
)
if not driver_exists:
try:
driver = color_alpha_mix.inputs[1].driver_add("default_value").driver
driver.type = 'SCRIPTED'
driver.expression = 'Alpha'
var = driver.variables.new()
var.name = 'Alpha'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = controller
var.targets[0].data_path = f"wow_m2_color_transparency.colors[{color_index}].alpha"
color_alpha_mix.label = color_alpha_mix.label.replace("OFF", "ON")
except Exception as e:
print(f"Error adding Color_Alpha_Mix driver: {e}")
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="Info: Drivers Enabled!", font_size=24, y_offset=67)
return {'FINISHED'}
@@ -0,0 +1,188 @@
import bpy
from ..panels.root_elements import is_obj_unused
class M2_OT_destroy_property(bpy.types.Operator):
bl_idname = "scene.wow_m2_destroy_wow_property"
bl_label = "Disable Property"
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
prop_group: bpy.props.StringProperty()
prop_map = {
'wow_m2_geoset': 'geosets',
'wow_m2_light': 'lights',
'wow_m2_material': 'materials'
}
@classmethod
def poll(cls, context):
return context.scene.wow_scene.type == 'M2' and hasattr(context, 'object') and context.object
def execute(self, context):
getattr(context.object, self.prop_group).enabled = False
col = getattr(bpy.context.scene.wow_m2_root_elements, self.prop_map[self.prop_group])
idx = col.find(context.object.name)
if idx >= 0:
col.remove(idx)
return {'FINISHED'}
class M2_OT_root_elements_components_change(bpy.types.Operator):
bl_idname = 'scene.wow_m2_root_elements_change'
bl_label = 'Add / Remove'
bl_description = 'Add / Remove'
bl_options = {'REGISTER', 'INTERNAL', 'UNDO'}
col_name: bpy.props.StringProperty(options={'HIDDEN'})
cur_idx_name: bpy.props.StringProperty(options={'HIDDEN'})
action: bpy.props.StringProperty(default='ADD', options={'HIDDEN'})
add_action: bpy.props.EnumProperty(
items=[('EMPTY', 'Empty', ''),
('NEW', 'New', '')],
default='EMPTY',
options={'HIDDEN'}
)
def execute(self, context):
if self.action == 'ADD':
if self.col_name != 'materials' \
and bpy.context.view_layer.objects.active \
and bpy.context.view_layer.objects.active.mode != 'OBJECT':
self.report({'ERROR'}, "Object mode must be active.")
return {'CANCELLED'}
if self.col_name == 'geosets':
print("trying to add geoset")
obj = bpy.context.view_layer.objects.active
if obj and obj.select_get():
if obj.type != 'MESH':
self.report({'ERROR'}, "Object must be a mesh")
return {'CANCELLED'}
if not is_obj_unused(obj):
if not obj.wow_m2_geoset.enabled:
self.report({'ERROR'}, "Object is already used")
return {'CANCELLED'}
else:
win = bpy.context.window
scr = win.screen
areas3d = [area for area in scr.areas if area.type == 'VIEW_3D']
region = [region for region in areas3d[0].regions if region.type == 'WINDOW']
override = {'window': win,
'screen': scr,
'area': areas3d[0],
'region': region[0],
'scene': bpy.context.scene,
'object': obj
}
bpy.ops.scene.wow_m2_destroy_wow_property(override, prop_group='wow_m2_geoset')
self.report({'INFO'}, "Geoset was overriden")
slot = bpy.context.scene.wow_m2_root_elements.geosets.add()
slot.pointer = obj
else:
bpy.context.scene.wow_m2_root_elements.geosets.add()
elif self.col_name == 'lights':
slot = bpy.context.scene.wow_m2_root_elements.lights.add()
if self.add_action == 'NEW':
light = bpy.data.objects.new(name='LIGHT', object_data=bpy.data.lights.new('LIGHT', type='POINT'))
bpy.context.collection.objects.link(light)
light.location = bpy.context.scene.cursor.location
slot.pointer = light
elif self.col_name == 'materials':
slot = bpy.context.scene.wow_m2_root_elements.materials.add()
if self.add_action == 'NEW':
new_mat = bpy.data.materials.new('Material')
slot.pointer = new_mat
elif self.col_name == 'textures':
slot = bpy.context.scene.wow_m2_root_elements.textures.add()
# if self.add_action == 'NEW':
# new_text = bpy.data.images.new('Texture')
# slot.pointer = new_text
elif self.col_name == 'attachments':
#TODO
bpy.context.scene.wow_m2_root_elements.attachments.add()
elif self.col_name == 'events':
#TODO
# bpy.ops.object.empty_add(type='CUBE', location=(0, 0, 0))
# obj = bpy.context.view_layer.objects.active
# obj.scale = (0.019463, 0.019463, 0.019463)
# bpy.ops.object.constraint_add(type='CHILD_OF')
# constraint = obj.constraints[-1]
# constraint.target = self.rig # TODO rig
# obj.parent = self.rig
# obj.name = "Event_{}".format(token)
# obj.wow_m2_event.enabled = True
slot = bpy.context.scene.wow_m2_root_elements.events.add()
# slot.pointer = obj
elif self.action == 'REMOVE':
col = getattr(context.scene.wow_m2_root_elements, self.col_name)
cur_idx = getattr(context.scene.wow_m2_root_elements, self.cur_idx_name)
if len(col) <= cur_idx:
return {'FINISHED'}
item = col[cur_idx].pointer
if item:
if self.col_name == 'geosets':
item.wow_m2_geoset.enabled = False
elif self.col_name == 'lights':
item.wow_m2_light.enabled = False
elif self.col_name == 'materials':
item.wow_m2_material.enabled = False
elif self.col_name == 'textures':
item.wow_m2_texture.enabled = False
elif self.col_name == 'attachments':
item.wow_m2_attachment.enabled = False
elif self.col_name == 'events':
item.wow_m2_event.enabled = False
col.remove(cur_idx)
else:
self.report({'ERROR'}, 'Unsupported token')
return {'CANCELLED'}
return {'FINISHED'}
@@ -0,0 +1,55 @@
import bpy
class M2_PT_animations_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_label = "M2 Animations"
def draw(self, context):
layout = self.layout
col = layout.column()
col.template_list("M2_UL_animation_editor_animation_list", "", context.scene, "wow_m2_animations", context.scene,
"wow_m2_cur_anim_index")
try:
cur_anim_track = context.scene.wow_m2_animations[context.scene.wow_m2_cur_anim_index]
row = col.row()
row_split = row.split().row(align=True)
row_split.prop(cur_anim_track, "playback_speed", text='Speed')
if context.scene.sync_mode == 'AUDIO_SYNC' and context.user_preferences.system.audio_device == 'JACK':
sub = row_split.row(align=True)
sub.scale_x = 2.0
sub.operator("screen.animation_play", text="", icon='PLAY')
row = row_split.row(align=True)
if not context.screen.is_animation_playing:
if context.scene.sync_mode == 'AUDIO_SYNC' and context.user_preferences.system.audio_device == 'JACK':
sub = row.row(align=True)
sub.scale_x = 2.0
sub.operator("screen.animation_play", text="", icon='PLAY')
else:
row.operator("screen.animation_play", text="", icon='PLAY_REVERSE').reverse = True
row.operator("screen.animation_play", text="", icon='PLAY')
row.operator("scene.wow_m2_animation_editor_play_global_sequence", text="", icon='SEQUENCE')
else:
sub = row.row(align=True)
sub.scale_x = 2.0
sub.operator("screen.animation_play", text="", icon='PAUSE')
row = row_split.row(align=True)
row.operator("scene.wow_m2_animation_editor_seq_deselect", text='', icon='ARMATURE_DATA')
col.separator()
except IndexError:
pass
col.operator('scene.wow_animation_editor_toggle', text='Edit animations', icon='SEQUENCE')
@classmethod
def poll(cls, context):
return context.scene is not None and context.scene.wow_scene.type == 'M2'
@@ -0,0 +1,62 @@
import bpy
from ..enums import *
class M2_PT_attachment_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "M2 Attachment"
def draw_header(self, context):
self.layout.prop(context.object.wow_m2_attachment, "enabled", text="")
def draw(self, context):
layout = self.layout
layout.enabled = context.object.wow_m2_attachment.enabled
col = layout.column()
col.prop(context.object.wow_m2_attachment, 'type', text="Type")
col.prop(context.object.wow_m2_attachment, 'animate', text="Animate")
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.object is not None
and context.object.type == 'EMPTY'
and not (context.object.wow_m2_event.enabled
or context.object.wow_m2_uv_transform.enabled
or context.object.wow_m2_camera.enabled
or context.object.wow_m2_ribbon.enabled
or context.object.wow_m2_particle.enabled)
)
class WowM2AttachmentPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name='Enabled',
description='Enabled this object to be a WoW M2 attachment point',
default=False
)
type: bpy.props.EnumProperty(
name="Type",
description="WoW Attachment Type",
items=get_attachment_types
)
animate: bpy.props.BoolProperty(
name='Animate',
description='Animate attached object at this keyframe',
default=True
)
def register():
bpy.types.Object.wow_m2_attachment = bpy.props.PointerProperty(type=WowM2AttachmentPropertyGroup)
def unregister():
del bpy.types.Object.wow_m2_attachment
@@ -0,0 +1,72 @@
import bpy
from ..enums import *
class M2_PT_bone_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "bone"
bl_label = "M2 Bone"
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(context.edit_bone.wow_m2_bone, "key_bone_id")
col.separator()
col.prop(context.edit_bone.wow_m2_bone, "flags")
col.separator()
col.prop(context.edit_bone.wow_m2_bone, "sort_index")
col.separator()
col.prop(context.edit_bone.wow_m2_bone, "submesh_id")
col.separator()
col.prop(context.edit_bone.wow_m2_bone, "bone_name_crc")
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.edit_bone is not None)
class WowM2BonePropertyGroup(bpy.types.PropertyGroup):
key_bone_id: bpy.props.EnumProperty(
name="Keybone",
description="WoW bone keybone ID",
items=get_keybone_ids
)
flags: bpy.props.EnumProperty(
name="Bone flags",
description="WoW bone flags",
items=BONE_FLAGS,
options={"ENUM_FLAG"}
)
sort_index: bpy.props.IntProperty(
name='Sort Index',
description="Used to sort bones when exporting M2 files. All bones must have this >= 0 or this setting is ignored",
default=-1
)
submesh_id: bpy.props.IntProperty(
name='Unknown value #1',
description="Unused (submesh_id)",
min=0,
max=65535,
default=0
)
bone_name_crc: bpy.props.IntProperty(
name='Unknown value #2 (bone_name_crc)',
description="Unused (bone_name_crc) (stored as int32, most readers read it as a uint32)",
default=0
)
def register():
bpy.types.EditBone.wow_m2_bone = bpy.props.PointerProperty(type=WowM2BonePropertyGroup)
def unregister():
del bpy.types.EditBone.wow_m2_bone
@@ -0,0 +1,537 @@
from math import ceil
import bpy
from ....utils.misc import wrap_text
class CameraErrors:
INVALID_OBJ = 0
DUPLICATE_OBJ = 1
BAD_GEOMETRY = 2
def update_follow_path_constraints(self, context):
for constraint in context.object.constraints:
if constraint.type == 'FOLLOW_PATH':
constraint.driver_remove("offset_factor")
constraint.driver_remove("influence")
context.object.constraints.remove(constraint)
for segment in context.object.wow_m2_camera.animation_curves:
# add follow path constraint
follow_path = context.object.constraints.new('FOLLOW_PATH')
follow_path.name = 'M2FollowPath'
follow_path.use_fixed_location = True
follow_path.target = segment.object
# drive offset
offset_fcurve = follow_path.driver_add("offset_factor")
driver = offset_fcurve.driver
driver.type = 'SCRIPTED'
driver.use_self = True
obj_name_var = driver.variables.new()
obj_name_var.name = 'obj_name'
obj_name_var.targets[0].id_type = 'OBJECT'
obj_name_var.targets[0].id = context.object
obj_name_var.targets[0].data_path = 'name'
driver.expression = 'calc_segment_offset(self, bpy.data.objects[obj_name], frame)'
# drive influence
influence_fcurve = follow_path.driver_add("influence")
driver = influence_fcurve.driver
driver.type = 'SCRIPTED'
driver.use_self = True
obj_name_var = driver.variables.new()
obj_name_var.name = 'obj_name'
obj_name_var.targets[0].id_type = 'OBJECT'
obj_name_var.targets[0].id = context.object
obj_name_var.targets[0].data_path = 'name'
driver.expression = 'in_path_segment(self, bpy.data.objects[obj_name], frame)'
# ensure track to constrain stays on top
if context.object.type == 'CAMERA':
context.object.wow_m2_camera.target = context.object.wow_m2_camera.target
def validate_camera_path(m2_camera):
errors = []
checked_curve_objs = []
for i, curve in enumerate(m2_camera.animation_curves):
# check if the used curve is None or invalid
if curve.object is None or curve.object.name not in bpy.context.scene.objects:
errors.append(("Curve slot #{} is invalid.".format(i), CameraErrors.INVALID_OBJ))
continue
# check if the same path is used twice
if curve.object in checked_curve_objs:
errors.append(("Curve \"{}\" is used more than once.".format(curve.object.name), CameraErrors.DUPLICATE_OBJ))
checked_curve_objs.append(curve.object)
# check curve's geometry validity
if len(curve.object.data.splines) > 1:
errors.append(("Curve \"{}\" contains more than 1 spline.".format(curve.object.name),
CameraErrors.BAD_GEOMETRY))
continue
if len(curve.object.data.splines[0].bezier_points) > 2:
errors.append(("Curve \"{}\" contains more than 2 bezier points.".format(curve.object.name),
CameraErrors.BAD_GEOMETRY))
continue
next_index = i + 1
if len(m2_camera.animation_curves) > next_index:
next_segment = m2_camera.animation_curves[next_index]
if not next_segment.object \
or not len(next_segment.object.data.splines) \
or not len(next_segment.object.data.splines[0].bezier_points):
continue
last_point = curve.object.data.splines[0].bezier_points[1]
next_point = next_segment.object.data.splines[0].bezier_points[0]
if last_point.co != next_point.co \
or last_point.handle_left != next_point.handle_left \
or last_point.handle_right != next_point.handle_right:
errors.append(("Curve \"{}\" does not connect to next segment properly.".format(curve.object.name),
CameraErrors.BAD_GEOMETRY))
return errors
def poll_camera_path_curve(self, obj):
return obj.type == 'CURVE' and len(obj.data.splines) == 1 and len(obj.data.splines[0].bezier_points)
def update_camera_path_curve(self, context):
# double check is needed because pippete selector ignores polling
if self.object:
self.name = self.object.name
if not poll_camera_path_curve(None, self.object):
self.object = None
self.name = ""
update_follow_path_constraints(None, context)
def update_empty_display_type(self, context):
context.object.empty_display_type = 'CONE'
def update_scene_animation(self, context):
context.scene.wow_m2_cur_anim_index = context.scene.wow_m2_cur_anim_index
def update_camera_target(self, context):
if self.target is not None and (self.target.type != 'EMPTY' or not self.target.wow_m2_camera.enabled):
self.target = None
try:
context.object.constraints.remove(context.object.constraints['M2TrackTo'])
except KeyError:
pass
return
if self.target is None:
try:
context.object.constraints.remove(context.object.constraints['M2TrackTo'])
except KeyError:
pass
return
try:
context.object.constraints['M2TrackTo'].target = self.target
except KeyError:
# add track_to contraint to camera to make it face the target object
track_to = context.object.constraints.new('TRACK_TO')
track_to.name = 'M2TrackTo'
track_to.up_axis = 'UP_Y'
track_to.track_axis = 'TRACK_NEGATIVE_Z'
track_to.use_target_z = True
track_to.target = self.target
finally:
# move camera down the constraints stack
ctx_override = bpy.context.copy()
track_to = context.object.constraints['M2TrackTo']
ctx_override["constraint"] = track_to
for _ in range(len(context.object.constraints) - 1):
bpy.ops.constraint.move_down(ctx_override, constraint=track_to.name, owner='OBJECT')
class M2_PT_camera_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "M2 Camera"
def draw_header(self, context):
if context.object.type == 'EMPTY':
self.layout.prop(context.object.wow_m2_camera, 'enabled', text='')
def draw(self, context):
layout = self.layout
layout.use_property_split = True
col = layout.column()
if context.object.type == 'CAMERA':
col.prop(context.object.wow_m2_camera, 'type')
col.prop(context.object.wow_m2_camera, 'target')
col.separator()
else:
layout.enabled = context.object.wow_m2_camera.enabled
self.bl_label = 'M2 Camera Target'
col.label(text='Path curves:')
row = col.row()
sub_col1 = row.column()
sub_col1.template_list("M2_UL_camera_curve_list", "", context.object.wow_m2_camera, "animation_curves",
context.object.wow_m2_camera, "cur_anim_curve_index")
sub_col_parent = row.column()
sub_col2 = sub_col_parent.column(align=True)
sub_col2.operator("object.wow_m2_camera_curve_add", text='', icon='ADD')
sub_col2.operator("object.wow_m2_camera_curve_remove", text='', icon='REMOVE')
sub_col_parent.separator()
sub_col3 = sub_col_parent.column(align=True)
sub_col3.operator("object.wow_m2_camera_curve_move", text='', icon='TRIA_UP').direction = 'UP'
sub_col3.operator("object.wow_m2_camera_curve_move", text='', icon='TRIA_DOWN').direction = 'DOWN'
split = col.row().split(factor=0.94)
row_split = split.row(align=True)
row_split.operator("object.wow_m2_camera_curve_compose", text='Compose curve', icon='RNDCURVE')
row_split.operator("object.wow_m2_camera_curve_decompose", text='Decompose curve', icon='CURVE_PATH')
split.row()
# draw error box
errors = sorted(validate_camera_path(context.object.wow_m2_camera), key=lambda x: x[1])
if errors:
col.separator()
col.label(text='Errors:')
box = col.box()
for error_msg, error_code in errors:
sub_box = box.box()
lines = wrap_text(ceil(bpy.context.area.width / 9), error_msg)
sub_box.row(align=True).label(text=lines[0], icon='ERROR')
for i in range(1, len(lines)):
sub_box.row(align=True).label(text=lines[i])
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.object is not None
and (context.object.type == 'CAMERA'
or (
context.object.type == 'EMPTY'
and not (context.object.wow_m2_attachment.enabled
or context.object.wow_m2_uv_transform.enabled
or context.object.wow_m2_event.enabled
or context.object.wow_m2_ribbon.enabled
or context.object.wow_m2_particle.enabled)
)
)
)
class WowM2CameraPathPropertyGroup(bpy.types.PropertyGroup):
object: bpy.props.PointerProperty(
type=bpy.types.Object,
name='Curve',
description='Bezier curve with only one spline with 2 bezier points, defining camera path for a frame range.',
poll=poll_camera_path_curve,
update=update_camera_path_curve
)
duration: bpy.props.FloatProperty(
name='Duration',
description="Duration in frames of the camera travelling along this path segment.",
min=0,
update=update_scene_animation
)
# for eevee use only
name: bpy.props.StringProperty()
this_object: bpy.props.PointerProperty(type=bpy.types.Object)
class WowM2CameraPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name='Enabled',
description='Enable this empty to be a camera target controller.',
default=False,
update=update_empty_display_type
)
type: bpy.props.EnumProperty(
name='Type',
description='Type of this camera',
items=[("0", "Portrait", "", 'OUTLINER_OB_ARMATURE', 0),
("1", "Character info", "", 'MOD_ARMATURE', 1),
("-1", "Flyby", "", 'FORCE_BOID', -1)]
)
animation_curves: bpy.props.CollectionProperty(type=WowM2CameraPathPropertyGroup)
cur_anim_curve_index: bpy.props.IntProperty()
target: bpy.props.PointerProperty(
type=bpy.types.Object,
name='Target',
description='Target of the camera. Can be animated using curves.',
poll=lambda self, obj: obj.type == 'EMPTY' and obj.wow_m2_camera.enabled,
update=update_camera_target
)
class M2_UL_camera_curve_list(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
self.use_filter_show = False
if self.layout_type in {'DEFAULT', 'COMPACT'}:
row = layout.row(align=True)
row1 = row.row(align=True)
row1.scale_x = 0.5
row1.label(text="#{}".format(index), icon='CURVE_BEZCURVE')
row.prop(item, "object", text="")
row.prop(item, "duration")
elif self.layout_type in {'GRID'}:
pass
class M2_OT_camera_curve_add(bpy.types.Operator):
bl_idname = 'object.wow_m2_camera_curve_add'
bl_label = 'Add curve segment'
bl_options = {'REGISTER', 'INTERNAL'}
def execute(self, context):
curve = context.object.wow_m2_camera.animation_curves.add()
curve.this_object = context.object
context.object.wow_m2_camera.cur_anim_curve_index = len(context.object.wow_m2_camera.animation_curves) - 1
update_follow_path_constraints(None, context)
return {'FINISHED'}
class M2_OT_camera_curve_remove(bpy.types.Operator):
bl_idname = 'object.wow_m2_camera_curve_remove'
bl_label = 'Remove curve segment'
bl_options = {'REGISTER', 'INTERNAL'}
def execute(self, context):
context.object.wow_m2_camera.animation_curves.remove(context.object.wow_m2_camera.cur_anim_curve_index)
update_follow_path_constraints(None, context)
return {'FINISHED'}
class M2_OT_camera_curve_move(bpy.types.Operator):
bl_idname = 'object.wow_m2_camera_curve_move'
bl_label = 'Move curve segment'
bl_options = {'REGISTER', 'INTERNAL'}
direction: bpy.props.StringProperty()
def execute(self, context):
if self.direction == 'UP':
context.object.wow_m2_camera.animation_curves.move(context.object.wow_m2_camera.cur_anim_curve_index,
context.object.wow_m2_camera.cur_anim_curve_index - 1)
context.object.wow_m2_camera.cur_anim_curve_index -= 1
elif self.direction == 'DOWN':
context.object.wow_m2_camera.animation_curves.move(context.object.wow_m2_camera.cur_anim_curve_index,
context.object.wow_m2_camera.cur_anim_curve_index + 1)
context.object.wow_m2_camera.cur_anim_curve_index += 1
else:
raise NotImplementedError("Only UP and DOWN movement in the UI list in supported.")
update_follow_path_constraints(None, context)
return {'FINISHED'}
class M2_OT_camera_curve_compose(bpy.types.Operator):
bl_idname = 'object.wow_m2_camera_curve_compose'
bl_label = 'Compose curve'
bl_description = 'Convert current curves to a single curve'
bl_options = {'REGISTER', 'INTERNAL'}
def execute(self, context):
if validate_camera_path(context.object.wow_m2_camera): # checking for errors
self.report({'ERROR'}, 'M2 curve path contains errors.')
return {'CANCELLED'}
camera_props = context.object.wow_m2_camera
curve = bpy.data.curves.new(name='BakedCurve', type='CURVE')
curve_obj = bpy.data.objects.new(name='BakedCurve', object_data=curve)
bpy.context.collection.objects.link(curve_obj)
curve.dimensions = '3D'
curve.resolution_u = 64
spline = curve.splines.new('BEZIER')
spline.resolution_u = 64
spline.bezier_points.add(count=len(camera_props.animation_curves))
curve_first_point = spline.bezier_points[0]
segment_first_point = camera_props.animation_curves[0].object.data.splines[0].bezier_points[0]
curve_first_point.co = segment_first_point.co
curve_first_point.handle_right = segment_first_point.handle_right
curve_first_point.handle_left = segment_first_point.handle_left
for i, segment in enumerate(camera_props.animation_curves):
segment_point = segment.object.data.splines[0].bezier_points[1]
curve_point = spline.bezier_points[i + 1]
curve_point.co = segment_point.co
curve_point.handle_right = segment_point.handle_right
curve_point.handle_left = segment_point.handle_left
return {'FINISHED'}
class M2_OT_camera_curve_decompose(bpy.types.Operator):
bl_idname = 'object.wow_m2_camera_curve_decompose'
bl_label = 'Decompose curve'
bl_description = 'Convert a single curves to timed curve segments'
bl_options = {'REGISTER', 'INTERNAL'}
curve_obj: bpy.props.StringProperty(
name='Curve',
description='Curve object to be decomposed to timed curve segments'
)
preserve_timing: bpy.props.BoolProperty(
name='Preserve Timing',
description='Align decomposed curve segments to already existing timed slots.',
default=True
)
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop_search(self, 'curve_obj', context.scene, 'objects', icon='OBJECT_DATA')
layout.prop(self, 'preserve_timing')
def execute(self, context):
try:
curve_obj = bpy.data.objects[self.curve_obj]
except KeyError:
self.report({'ERROR'}, 'No curve object is selected.')
return {'FINISHED'}
if curve_obj.type != 'CURVE':
self.report({'ERROR'}, 'Select a bezier curve object instead.')
return {'FINISHED'}
if not curve_obj.data.splines or len(curve_obj.data.splines[0].bezier_points) < 2:
self.report({'ERROR'}, 'Badly formed curve.')
return {'FINISHED'}
if not curve_obj.data.splines[0].type == 'BEZIER':
self.report({'ERROR'}, 'Select a bezier curve object instead.')
return {'FINISHED'}
camera_props = context.object.wow_m2_camera
# remove old referenced segment objects
for segment in camera_props.animation_curves:
old_segment = segment.object
segment.object = None
bpy.data.objects.remove(old_segment, do_unlink=True)
# remove slots based on user's choice
if not self.preserve_timing:
camera_props.animation_curves.clear()
# create a parent for curve segments
p_obj = bpy.data.objects.new("{}_Decomposed".format(curve_obj.name), None)
bpy.context.collection.objects.link(p_obj)
# create segment curves
new_segments = []
points = curve_obj.data.splines[0].bezier_points
for i in range(1, len(points)):
first_point = points[i - 1]
last_point = points[i]
name = "{}_BakedSegment".format(curve_obj.name)
curve = bpy.data.curves.new(name=name, type='CURVE')
new_obj = bpy.data.objects.new(name=name, object_data=curve)
bpy.context.collection.objects.link(new_obj)
new_obj.parent = p_obj
curve.dimensions = '3D'
curve.resolution_u = 64
spline = curve.splines.new('BEZIER')
spline.resolution_u = 64
spline.bezier_points.add(count=1)
seg_first_point = spline.bezier_points[0]
seg_last_point = spline.bezier_points[1]
seg_first_point.co = first_point.co
seg_first_point.handle_right = first_point.handle_right
seg_first_point.handle_left = first_point.handle_left
seg_last_point.co = last_point.co
seg_last_point.handle_right = last_point.handle_right
seg_last_point.handle_left = last_point.handle_left
new_segments.append(new_obj)
# fill in overlapping slots
segments_overlap = 0
for i, segment in enumerate(camera_props.animation_curves):
segment.object = new_segments[i]
segments_overlap += 1
# create new slots for extra segments
for i in range(segments_overlap, len(new_segments) - segments_overlap):
segment = camera_props.animation_curves.add()
segment.object = new_segments[i]
segment.this_object = context.object
update_follow_path_constraints(None, context)
return {'FINISHED'}
def register():
bpy.types.Object.wow_m2_camera = bpy.props.PointerProperty(type=WowM2CameraPropertyGroup)
def unregister():
del bpy.types.Object.wow_m2_camera
@@ -0,0 +1,181 @@
import bpy
# ---------------------------
# Helpers
# ---------------------------
def find_color_controller():
for col in bpy.data.collections:
# Look for collection that has a child called "Color_Transparency"
# This needs to be changed later to only fetch the controller belonging to the specific m2
if "Color_Transparency" in col.children:
subcol = col.children.get("Color_Transparency")
for obj in subcol.objects:
if obj.get("is_color_transparency_controller") or obj.name == "ColorTransparencyController":
return obj
return None
def draw_color_transparency_ui(layout, controller):
props = controller.wow_m2_color_transparency
# Colors (top)
layout.label(text="Colors")
layout.template_list("M2_UL_color_list", "", props, "colors", props, "cur_color_index")
row = layout.row(align=True)
row.operator("object.wow_m2_add_color", icon='ADD', text="")
row.operator("object.wow_m2_remove_color", icon='REMOVE', text="")
layout.separator() # spacing between sections
# Transparencies (bottom)
layout.label(text="Transparencies")
layout.template_list("M2_UL_transparency_list", "", props, "transparencies", props, "cur_trans_index")
row = layout.row(align=True)
row.operator("object.wow_m2_add_transparency", icon='ADD', text="")
row.operator("object.wow_m2_remove_transparency", icon='REMOVE', text="")
# ---------------------------
# Property groups
# ---------------------------
class WowM2SingleColor(bpy.types.PropertyGroup):
name: bpy.props.StringProperty()
color: bpy.props.FloatVectorProperty(
name='Color', subtype='COLOR',
default=(1.0, 1.0, 1.0), size=3, min=0.0, max=1.0
)
alpha: bpy.props.FloatProperty(
name="Alpha",
default=1.0,
min=0.0,
max=1.0
)
class WowM2SingleTransparency(bpy.types.PropertyGroup):
name: bpy.props.StringProperty()
value: bpy.props.FloatProperty(
name='Transparency', default=1.0, min=0.0, max=1.0
)
class WowM2ColorTransparencyPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(default=False)
colors: bpy.props.CollectionProperty(type=WowM2SingleColor)
transparencies: bpy.props.CollectionProperty(type=WowM2SingleTransparency)
cur_color_index: bpy.props.IntProperty(default=0)
cur_trans_index: bpy.props.IntProperty(default=0)
# ---------------------------
# UI lists and panel
# ---------------------------
class M2_UL_color_list(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
row = layout.row(align=True)
row.prop(item, "name", text="", emboss=False, icon='COLOR')
row.prop(item, "color", text="")
row.prop(item, "alpha", text="A")
class M2_UL_transparency_list(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
row = layout.row(align=True)
row.prop(item, "name", text="", emboss=False, icon='MOD_OPACITY')
row.prop(item, "value", text="")
class M2_PT_color_transparency_scene_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_label = "M2 Color / Transparency"
def draw(self, context):
layout = self.layout
controller = find_color_controller()
if not controller:
layout.label(text="No ColorTransparencyController found", icon='ERROR')
return
draw_color_transparency_ui(layout, controller)
class M2_PT_color_transparency_object_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "M2 Color / Transparency"
bl_parent_id = ""
@classmethod
def poll(cls, context):
obj = context.object
return obj and obj.get("is_color_transparency_controller")
def draw(self, context):
layout = self.layout
controller = context.object
draw_color_transparency_ui(layout, controller)
# ---------------------------
# Operators
# ---------------------------
class M2_OT_add_color(bpy.types.Operator):
bl_idname = "object.wow_m2_add_color"
bl_label = "Add Color"
def execute(self, context):
controller = find_color_controller()
if not controller: return {'CANCELLED'}
props = controller.wow_m2_color_transparency
c = props.colors.add()
c.name = f"Color_{len(props.colors)-1}"
props.cur_color_index = len(props.colors)-1
return {'FINISHED'}
class M2_OT_remove_color(bpy.types.Operator):
bl_idname = "object.wow_m2_remove_color"
bl_label = "Remove Color"
def execute(self, context):
controller = find_color_controller()
if not controller: return {'CANCELLED'}
props = controller.wow_m2_color_transparency
if props.colors:
props.colors.remove(props.cur_color_index)
return {'FINISHED'}
class M2_OT_add_transparency(bpy.types.Operator):
bl_idname = "object.wow_m2_add_transparency"
bl_label = "Add Transparency"
def execute(self, context):
controller = find_color_controller()
if not controller: return {'CANCELLED'}
props = controller.wow_m2_color_transparency
t = props.transparencies.add()
t.name = f"Transparency_{len(props.transparencies)-1}"
props.cur_trans_index = len(props.transparencies)-1
return {'FINISHED'}
class M2_OT_remove_transparency(bpy.types.Operator):
bl_idname = "object.wow_m2_remove_transparency"
bl_label = "Remove Transparency"
def execute(self, context):
controller = find_color_controller()
if not controller: return {'CANCELLED'}
props = controller.wow_m2_color_transparency
if props.transparencies:
props.transparencies.remove(props.cur_trans_index)
return {'FINISHED'}
# ---------------------------
# Registration
# ---------------------------
def register():
bpy.types.Object.wow_m2_color_transparency = bpy.props.PointerProperty(type=WowM2ColorTransparencyPropertyGroup)
def unregister():
del bpy.types.Object.wow_m2_color_transparency
@@ -0,0 +1,496 @@
import os
import bpy
from ....utils.misc import load_game_data
###############################
## Items callbacks
###############################
def get_creature_model_data(self, context):
game_data = load_game_data()
creature_model_data_db = game_data.db_files_client.CreatureModelData
cr_model_data_entries = [('None', 'None', '')]
model_path = os.path.splitext(context.scene.wow_scene.game_path.lower())[0]
model_path = model_path.replace('/', '\\')
for record in creature_model_data_db.records:
if os.path.splitext(record.ModelName.lower())[0] == model_path:
cr_model_data_entries.append((str(record.ID), 'ModelDataEntry_{}'.format(record.ID), ""))
return cr_model_data_entries
def get_creature_display_infos(self, context):
game_data = load_game_data()
creature_display_info_db = game_data.db_files_client.CreatureDisplayInfo
cr_display_infos = [('None', 'None', '')]
cr_model_data = int(context.scene.wow_m2_creature.CreatureModelData)
for record in creature_display_info_db.records:
if record.ModelID == cr_model_data:
cr_display_infos.append((str(record.ID), 'CreatureDisplayInfoEntry_{}'.format(record.ID), ""))
return cr_display_infos
def get_char_races(self, context):
game_data = load_game_data()
chr_races_db = game_data.db_files_client.ChrRaces
return [(str(record.ID), record.ClientFileString, '') for record in chr_races_db.records]
###############################
## Update callbacks
###############################
def load_display_info_properties(self, context):
game_data = load_game_data()
creature_display_info_db = game_data.db_files_client.CreatureDisplayInfo
record = creature_display_info_db[int(context.scene.wow_m2_creature.CreatureDisplayInfo)]
context.scene.wow_m2_creature.DisplaySound = record.SoundID
context.scene.wow_m2_creature.DisplayScale = record.CreatureModelScale
context.scene.wow_m2_creature.DisplayTexture1 = record.Texture1
context.scene.wow_m2_creature.DisplayTexture2 = record.Texture2
context.scene.wow_m2_creature.DisplayTexture3 = record.Texture3
context.scene.wow_m2_creature.DisplayPortraitTextureName = record.PortraitTextureName
context.scene.wow_m2_creature.ExtraDisplayInformation = record.ExtendedDisplayInfoID
def load_display_extra_properties(self, context):
game_data = load_game_data()
creature_display_info_extra_db = game_data.db_files_client.CreatureDisplayInfoExtra
record = creature_display_info_extra_db[int(context.scene.wow_m2_creature.ExtraDisplayInformation)]
if record:
context.scene.wow_m2_creature.DisplayExtraRace = str(record.DisplayRaceID)
context.scene.wow_m2_creature.DisplayExtraGender = str(record.DisplaySexID)
context.scene.wow_m2_creature.DisplayExtraSkinColor = record.SkinID
context.scene.wow_m2_creature.DisplayExtraFaceType = record.FaceID
context.scene.wow_m2_creature.DisplayExtraHairType = record.HairStyleID
context.scene.wow_m2_creature.DisplayExtraHairStyle = record.HairColorID
context.scene.wow_m2_creature.DisplayExtraBeardStyle = record.FacialHairID
context.scene.wow_m2_creature.DisplayExtraHelm = record.Helm
context.scene.wow_m2_creature.DisplayExtraShoulder = record.Shoulder
context.scene.wow_m2_creature.DisplayExtraShirt = record.Shirt
context.scene.wow_m2_creature.DisplayExtraCuirass = record.Cuirass
context.scene.wow_m2_creature.DisplayExtraBelt = record.Belt
context.scene.wow_m2_creature.DisplayExtraLegs = record.Legs
context.scene.wow_m2_creature.DisplayExtraBoots = record.Boots
context.scene.wow_m2_creature.DisplayExtraWrist = record.Wrist
context.scene.wow_m2_creature.DisplayExtraGloves = record.Gloves
context.scene.wow_m2_creature.DisplayExtraTabard = record.Tabard
context.scene.wow_m2_creature.DisplayExtraCape = record.Cape
context.scene.wow_m2_creature.DisplayExtraCanEquip = record.CanEquip
context.scene.wow_m2_creature.DisplayExtraTexture = record.Texture
elif int(context.scene.wow_m2_creature.ExtraDisplayInformation):
context.scene.wow_m2_creature.ExtendedDisplayInfoID = 0
###############################
## User Interface
###############################
#### Pop-up dialog ####
class M2_OT_creature_editor_dialog(bpy.types.Operator):
bl_idname = 'scene.wow_creature_editor_toggle'
bl_label = 'WoW M2 Creature Editor'
@classmethod
def poll(cls, context):
return context.scene is not None and context.scene.wow_scene.type == 'M2'
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
split = layout.split(factor=0.5)
col = split.column()
col1 = split.column()
if not context.scene.wow_scene.game_path:
col.label(text='Model path is unknown. Fill Game Path in scene properties.', icon='ERROR')
return
col.label(text='Model data:')
col.prop(context.scene.wow_m2_creature, 'CreatureModelData', text='')
col.separator()
if context.scene.wow_m2_creature.CreatureModelData != 'None':
col1.label(text='Display Info:')
col1.prop(context.scene.wow_m2_creature, 'CreatureDisplayInfo', text='')
if context.scene.wow_m2_creature.CreatureDisplayInfo != 'None':
col1.label(text='Settings:', icon='SETTINGS')
box = col1.box()
box.prop(context.scene.wow_m2_creature, 'DisplaySound')
box.prop(context.scene.wow_m2_creature, 'DisplayScale')
box.separator()
box.operator('scene.wow_creature_load_textures',
text='Load all textures',
icon='APPEND_BLEND').LoadAll = True
row = box.row(align=True)
row.prop(context.scene.wow_m2_creature, 'DisplayTexture1')
op = row.operator('scene.wow_creature_load_textures', text='', icon='APPEND_BLEND')
op.LoadAll = False
op.Path = context.scene.wow_m2_creature.DisplayTexture1
op.TexNum = 11
row = box.row(align=True)
row.prop(context.scene.wow_m2_creature, 'DisplayTexture2')
op = row.operator('scene.wow_creature_load_textures', text='', icon='APPEND_BLEND')
op.LoadAll = False
op.Path = context.scene.wow_m2_creature.DisplayTexture2
op.TexNum = 12
row = box.row(align=True)
row.prop(context.scene.wow_m2_creature, 'DisplayTexture3')
op = row.operator('scene.wow_creature_load_textures', text='', icon='APPEND_BLEND')
op.LoadAll = False
op.Path = context.scene.wow_m2_creature.DisplayTexture3
op.TexNum = 13
box.prop(context.scene.wow_m2_creature, 'DisplayPortraitTextureName')
box.prop(context.scene.wow_m2_creature, 'ExtraDisplayInformation')
if context.scene.wow_m2_creature.ExtraDisplayInformation != 0:
box.label(text='Settings:', icon='SETTINGS')
box1 = box.box()
box1.prop(context.scene.wow_m2_creature, 'DisplayExtraCanEquip')
box1.prop(context.scene.wow_m2_creature, 'DisplayExtraTexture')
box1.prop(context.scene.wow_m2_creature, 'DisplayExtraRace')
box1.prop(context.scene.wow_m2_creature, 'DisplayExtraGender')
box1.prop(context.scene.wow_m2_creature, 'DisplayExtraSkinColor')
box1.prop(context.scene.wow_m2_creature, 'DisplayExtraFaceType')
box1.prop(context.scene.wow_m2_creature, 'DisplayExtraHairType')
box1.prop(context.scene.wow_m2_creature, 'DisplayExtraHairStyle')
box1.prop(context.scene.wow_m2_creature, 'DisplayExtraBeardStyle')
box1.label(text='Equipment:')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraHelm')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraShoulder')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraShirt')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraCuirass')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraBelt')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraLegs')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraBoots')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraWrist')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraGloves')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraTabard')
row = box1.row()
row.prop(context.scene.wow_m2_creature, 'DisplayExtraCape')
else:
col.label(text='No display info found.', icon='PMARKER_ACT')
def check(self, context): # redraw the popup window
return True
class WowM2CreaturePropertyGroup(bpy.types.PropertyGroup):
# DBs
CreatureModelData: bpy.props.EnumProperty(
name='Model data',
description='CreatureModelData.db entry',
items=get_creature_model_data
)
CreatureDisplayInfo: bpy.props.EnumProperty(
name='Creature display',
description='CreatureDisplayInfo.db entry',
items=get_creature_display_infos,
update=load_display_info_properties
)
ExtraDisplayInformation: bpy.props.IntProperty(
name='Display extra',
description='Applies only to NPCs with character model (hair/facial feature/... and equipment settings). Not used for creatures.',
default=0,
min=0,
update=load_display_extra_properties
)
# Creature display info
DisplaySound: bpy.props.IntProperty(
name='Sound',
description='If 0 - CreatureModelData information is used. Otherwise, overrides generic model settings for this displayID.',
default=0,
min=0
)
DisplayScale: bpy.props.FloatProperty(
name='Scale',
description='Default scale. Stacks (by multiplying) with other scale settings (in creature_template, applied auras...).',
default=1.0,
min=0.0
)
DisplayTexture1: bpy.props.StringProperty(
name='Texture 1',
description='First creature skin texture. Texture must be in the same folder as the model.',
)
DisplayTexture2: bpy.props.StringProperty(
name='Texture 2',
description='Second creature skin texture. Texture must be in the same folder as the model.',
)
DisplayTexture3: bpy.props.StringProperty(
name='Texture 3',
description='Third creature skin texture. Texture must be in the same folder as the model.',
)
DisplayPortraitTextureName: bpy.props.StringProperty(
name='Portrait Texture',
description='Holding an icon like INV_Misc_Food_59. Only on a few.',
)
# Creature display info extra
DisplayExtraRace: bpy.props.EnumProperty(
name='Race',
description='The race this NPC belongs to',
items=get_char_races
)
DisplayExtraGender: bpy.props.EnumProperty(
name='Gender',
description='0 for Male, 1 for Female',
items=[('0', 'Male', ''),
('1', 'Female', '')]
)
DisplayExtraSkinColor: bpy.props.IntProperty(
name='Skin Color',
default=0,
min=0
)
DisplayExtraFaceType: bpy.props.IntProperty(
name='Face type',
default=0,
min=0
)
DisplayExtraHairType: bpy.props.IntProperty(
name='Hair type',
default=0,
min=0
)
DisplayExtraHairStyle: bpy.props.IntProperty(
name='Hairstyle',
default=0,
min=0
)
DisplayExtraBeardStyle: bpy.props.IntProperty(
name='Beard',
default=0,
min=0
)
DisplayExtraHelm: bpy.props.IntProperty(
name='Helm',
default=0,
min=0
)
DisplayExtraShoulder: bpy.props.IntProperty(
name='Shoulder',
default=0,
min=0
)
DisplayExtraShirt: bpy.props.IntProperty(
name='Shirt',
default=0,
min=0
)
DisplayExtraCuirass: bpy.props.IntProperty(
name='Cuirass',
default=0,
min=0
)
DisplayExtraBelt: bpy.props.IntProperty(
name='Belt',
default=0,
min=0
)
DisplayExtraLegs: bpy.props.IntProperty(
name='Legs',
default=0,
min=0
)
DisplayExtraBoots: bpy.props.IntProperty(
name='Boots',
default=0,
min=0
)
DisplayExtraWrist: bpy.props.IntProperty(
name='Wrist',
default=0,
min=0
)
DisplayExtraGloves: bpy.props.IntProperty(
name='Gloves',
default=0,
min=0
)
DisplayExtraTabard: bpy.props.IntProperty(
name='Tabard',
default=0,
min=0
)
DisplayExtraCape: bpy.props.IntProperty(
name='Cape',
default=0,
min=0
)
DisplayExtraCanEquip: bpy.props.BoolProperty(
name='Can equip',
default=True,
)
DisplayExtraTexture: bpy.props.StringProperty(
name='Texture'
)
###############################
## Operators
###############################
class M2_OT_creature_editor_load_textures(bpy.types.Operator):
bl_idname = 'scene.wow_creature_load_textures'
bl_label = 'Load creature skins'
bl_description = 'Loads skin textures on import .M2'
bl_options = {'REGISTER', 'INTERNAL'}
Path: bpy.props.StringProperty()
TexNum: bpy.props.IntProperty()
LoadAll: bpy.props.BoolProperty()
@staticmethod
def load_skin_texture(context, path, tex_type):
from ....ui.preferences import get_project_preferences
cache_dir = get_project_preferences().cache_dir_path
game_data = load_game_data()
game_data.extract_textures_as_png(cache_dir, [path + '.blp'])
img = None
try:
img = bpy.data.images.load(os.path.join(cache_dir, path + '.png'))
except RuntimeError:
pass
if img:
for obj in filter(lambda o: o.type == 'MESH' and not o.wow_m2_geoset.collision_mesh, context.scene.objects):
for i, material in enumerate(obj.data.materials):
# OLD by skarn, not comaptible anymore
# if material.active_texture.wow_m2_texture.texture_type == str(tex_type):
# material.active_texture.image = img
#
# uv = obj.data.uv_layers.active
# for poly in obj.data.polygons:
# if poly.material_index == i:
# uv.data[poly.index].image = img
# if material.active_texture.wow_m2_texture.texture_type == str(tex_type):
# material.active_texture.image = img
#if material.wow_m2_material.texture_1:
# if material.wow_m2_material.texture_1.image.wow_m2_texture.texture_type == str(tex_type):
if material.node_tree.nodes.get('Tex1_image').image: # which one is better to use ?
if material.node_tree.nodes.get('Tex1_image').image.wow_m2_texture.texture_type == str(tex_type):
img.name = material.node_tree.nodes.get('Tex1_image').image.name
img.wow_m2_texture.texture_type = str(tex_type)
img.wow_m2_texture.flags = material.node_tree.nodes.get('Tex1_image').image.wow_m2_texture.flags
img.wow_m2_texture.path = material.node_tree.nodes.get('Tex1_image').image.wow_m2_texture.path
img.wow_m2_texture.enabled = True
material.wow_m2_material.texture_1 = img # this just changes the m2 mat to a newly created texture with incorrect m2 text settings
# can try to replace the m2 texture by the correct texture
# change the shader tetxures, looks like the best replacement over active texture
material.node_tree.nodes.get('Tex1_image').image = img
# TODO : multiple textures ?
def execute(self, context):
if not context.scene.wow_scene.game_path:
self.report({'ERROR'}, 'Path to model is unknown.')
return {'CANCELLED'}
base_path = os.path.dirname(context.scene.wow_scene.game_path)
if self.LoadAll:
if context.scene.wow_m2_creature.DisplayTexture1:
self.load_skin_texture(context,
os.path.join(base_path, context.scene.wow_m2_creature.DisplayTexture1),
11)
if context.scene.wow_m2_creature.DisplayTexture2:
self.load_skin_texture(context,
os.path.join(base_path, context.scene.wow_m2_creature.DisplayTexture2),
12)
if context.scene.wow_m2_creature.DisplayTexture3:
self.load_skin_texture(context,
os.path.join(base_path, context.scene.wow_m2_creature.DisplayTexture3),
13)
else:
if self.Path:
self.load_skin_texture(context, os.path.join(base_path, self.Path), self.TexNum)
else:
self.report({'ERROR'}, "No texture to load.")
return {'ERROR'}
self.report({'INFO'}, "Successfully loaded creature skins.")
return {'FINISHED'}
def register():
bpy.types.Scene.wow_m2_creature = bpy.props.PointerProperty(type=WowM2CreaturePropertyGroup)
def unregister():
del bpy.types.Scene.wow_m2_creature
@@ -0,0 +1,82 @@
import bpy
from ..enums import *
def update_event_name(self, context):
event_type = context.object.wow_m2_event.token
context.object.name = ('Event_' + M2EventTokens.get_event_name(event_type))
class M2_PT_event_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "M2 Event"
def draw_header(self, context):
self.layout.prop(context.object.wow_m2_event, "enabled", text="")
def draw(self, context):
layout = self.layout
layout.enabled = context.object.wow_m2_event.enabled
col = layout.column()
col.prop(context.object.wow_m2_event, 'token')
event_name = M2EventTokens.get_event_name(context.object.wow_m2_event.token)
if event_name in ('PlayEmoteSound', 'DoodadSoundUnknown', 'DoodadSoundOneShot', 'GOPlaySoundKitCustom'):
col.label(text='SoundEntryID')
col.prop(context.object.wow_m2_event, 'data')
elif event_name == 'GOAddShake':
col.label(text='SpellEffectCameraShakesID')
col.prop(context.object.wow_m2_event, 'data')
col.prop(context.object.wow_m2_event, 'fire',text="Event Activated")
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.object is not None
and context.object.type == 'EMPTY'
and not (context.object.wow_m2_attachment.enabled
or context.object.wow_m2_uv_transform.enabled
or context.object.wow_m2_camera.enabled
or context.object.wow_m2_ribbon.enabled
or context.object.wow_m2_particle.enabled)
)
class WowM2EventPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name='Enabled',
description='Enabled this object to be a WoW M2 event',
default=False
)
token: bpy.props.EnumProperty(
name='Token',
description='This token defines the purpose of the event',
items=get_event_names,
update=update_event_name
)
data: bpy.props.IntProperty(
name='Data',
description='Data passed when this event is fired',
min=0
)
fire: bpy.props.IntProperty(
name='Fire',
description='Enable this event in this specific animation keyframe',
default = 1,
min=1,
max=1
)
def register():
bpy.types.Object.wow_m2_event = bpy.props.PointerProperty(type=WowM2EventPropertyGroup)
def unregister():
del bpy.types.Object.wow_m2_event
@@ -0,0 +1,57 @@
from collections import namedtuple
import bpy
from ..enums import *
class M2_PT_geoset_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "M2 Geoset"
def draw(self, context):
self.layout.prop(context.object.wow_m2_geoset, "collision_mesh")
if not context.object.wow_m2_geoset.collision_mesh:
self.layout.prop(context.object.wow_m2_geoset, "mesh_part_group")
self.layout.prop(context.object.wow_m2_geoset, "mesh_part_id")
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.object is not None
and context.object.data is not None
and isinstance(context.object.data, bpy.types.Mesh))
class WowM2GeosetPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty()
collision_mesh: bpy.props.BoolProperty(
name='Collision mesh',
default=False
)
mesh_part_group: bpy.props.EnumProperty(
name="Geoset group",
description="Group of this geoset",
items=MESH_PART_TYPES
)
mesh_part_id: bpy.props.EnumProperty(
name="Geoset ID",
description="Mesh part ID of this geoset",
items=mesh_part_id_menu
)
#############
def register():
bpy.types.Object.wow_m2_geoset = bpy.props.PointerProperty(type=WowM2GeosetPropertyGroup)
def unregister():
del bpy.types.Object.wow_m2_geoset
@@ -0,0 +1,96 @@
import bpy
from ..enums import GLOBAL_FLAGS
# ---------------------------
# Property Group
# ---------------------------
class WowM2globalflagsPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name="Enabled",
description="Enable M2 global flags for this collection",
default=False,
)
flagsLK: bpy.props.EnumProperty(
name="WotLK Flags",
description="M2 global flags used in WotLK",
items=GLOBAL_FLAGS[:5],
options={"ENUM_FLAG"},
)
flagsLegion: bpy.props.EnumProperty(
name="Legion Flags",
description="M2 global flags used in Legion/Retail",
items=GLOBAL_FLAGS[5:],
options={"ENUM_FLAG"},
)
# ---------------------------
# User Interface
# ---------------------------
class M2_PT_global_flags_panel(bpy.types.Panel):
bl_label = "M2 Global Flags"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "collection"
@classmethod
def poll(cls, context):
col = getattr(context, "collection", None)
if not col:
return False
# Only show on collections marked as M2 roots
return col.get("wow_m2_collection", False)
def draw_header(self, context):
self.layout.prop(context.collection.wow_m2_globalflags, "enabled", text="")
def draw(self, context):
col = context.collection
flags = col.wow_m2_globalflags
scene = context.scene
layout = self.layout
layout.enabled = flags.enabled
box = layout.box()
box.label(text="Global Flags", icon="WORLD_DATA")
# Determine version (2 = WotLK, 6 = Legion/Retail)
if hasattr(scene, "wow_scene"):
version = scene.wow_scene.version
else:
version = "2"
# --- WotLK flags ---
box.label(text="WotLK Flags")
wl_box = box.column(align=True)
from ..enums import GLOBAL_FLAGS # ensure correct import path if needed
for identifier, label, tooltip, icon, bit in GLOBAL_FLAGS[:5]:
wl_box.prop_enum(flags, "flagsLK", identifier, text=label, icon=icon)
# --- Legion flags (only if version >= 6, else optional) ---
if version == '6':
box.separator()
box.label(text="Legion Flags")
lg_box = box.column(align=True)
for identifier, label, tooltip, icon, bit in GLOBAL_FLAGS[5:]:
lg_box.prop_enum(flags, "flagsLegion", identifier, text=label, icon=icon)
# ---------------------------
# Register
# ---------------------------
def register():
bpy.types.Collection.wow_m2_globalflags = bpy.props.PointerProperty(type=WowM2globalflagsPropertyGroup)
def unregister():
del bpy.types.Collection.wow_m2_globalflags
@@ -0,0 +1,108 @@
import bpy
class M2_PT_light_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "data"
bl_label = "M2 Light"
def draw_header(self, context):
self.layout.prop(context.object.wow_m2_attachment, "enabled", text="")
def draw(self, context):
layout = self.layout
layout.enabled = context.object.data.wow_m2_light.enabled
col = layout.column()
col.prop(context.object.data.wow_m2_light, "type")
col.prop(context.object.data.wow_m2_light, "ambient_color")
col.prop(context.object.data.wow_m2_light, "ambient_intensity")
col.prop(context.object.data.wow_m2_light, "diffuse_color")
col.prop(context.object.data.wow_m2_light, "diffuse_intensity")
col.prop(context.object.data.wow_m2_light, "attenuation_start")
col.prop(context.object.data.wow_m2_light, "attenuation_end")
col.prop(context.object.data.wow_m2_light, "visibility")
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.object is not None
and context.object.type == 'LIGHT')
def update_lamp_type(self, context):
context.object.data.type = 'POINT' if int(context.object.data.wow_m2_light.type) else 'SPOT'
class WowM2LightPropertyGroup(bpy.types.PropertyGroup):
type: bpy.props.EnumProperty(
name="type",
description="WoW M2 light type",
items=[('0', 'Directional', 'Login screen only'), ('1', 'Point', '')],
default='1',
update=update_lamp_type
)
ambient_color: bpy.props.FloatVectorProperty(
name="Color",
subtype='COLOR',
default=(1, 1, 1),
min=0.0,
max=1.0
)
ambient_intensity: bpy.props.FloatProperty(
name="Ambient intensity",
description="Ambient intensity of the light",
default=1.0,
min=0.0,
max=1.0
)
diffuse_color: bpy.props.FloatVectorProperty(
name="Color",
subtype='COLOR',
default=(1, 1, 1),
min=0.0,
max=1.0
)
diffuse_intensity: bpy.props.FloatProperty(
name="Diffuse intensity",
description="Diffuse intensity of the light",
default=1.0,
min=0.0,
max=1.0
)
attenuation_start: bpy.props.FloatProperty(
name="Attenuation start",
description="Start of attenuation",
min=0.0 # TODO: max / default?
)
attenuation_end: bpy.props.FloatProperty(
name="Attenuation end",
description="End of attenuation",
min=0.0 # TODO: max / default?
)
visibility: bpy.props.BoolProperty(
name='Visible',
default=True
)
enabled: bpy.props.BoolProperty(
name='Enabled',
default=False
)
def register():
bpy.types.Light.wow_m2_light = bpy.props.PointerProperty(type=WowM2LightPropertyGroup)
def unregister():
del bpy.types.Light.wow_m2_light
@@ -0,0 +1,68 @@
import bpy
from ... import util
# ---------------------------
# Property + Update Callback
# ---------------------------
def m2_collection_enable_update(self, context):
"""Called when user toggles the enable checkbox."""
try:
if self.wow_enable_m2_collection:
# Run collection setup
util.get_or_create_m2_collection(self)
# Disable the checkbox permanently once used
self.wow_enable_m2_collection = False
bpy.ops.wm.redraw_timer(type='DRAW_WIN', iterations=1)
except Exception as e:
print(f"[M2] Failed to initialize M2 Collection: {e}")
# ---------------------------
# UI Panel (Collection Properties Tab)
# ---------------------------
class M2_PT_collection(bpy.types.Panel):
bl_label = "M2 Collection"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "collection"
@classmethod
def poll(cls, context):
col = context.collection
if not col:
return False
# Must be a root collection (direct child of scene)
is_root = any(child is col for child in context.scene.collection.children)
return is_root
def draw(self, context):
col = context.collection
layout = self.layout
box = layout.box()
if col.get("wow_m2_collection", False):
box.label(text="This collection is an M2 Collection", icon="CHECKMARK")
box.label(text="It contains M2 scene data & controllers.")
else:
box.label(text="Convert this collection to M2 structure", icon="MOD_BUILD")
box.prop(col, "wow_enable_m2_collection", text="Enable")
# ---------------------------
# Registration
# ---------------------------
def register():
bpy.types.Collection.wow_enable_m2_collection = bpy.props.BoolProperty(
name="Enable",
description="Convert this collection into an M2 collection",
default=False,
update=m2_collection_enable_update,
)
def unregister():
del bpy.types.Collection.wow_enable_m2_collection
@@ -0,0 +1,752 @@
import bpy
from ..enums import *
from ...bl_render.cycles import update_m2_mat_node_tree_cycles
from ... import util
class TexturePathDefaultButton(bpy.types.Operator):
bl_idname = "wow_m2_texture.set_default_texture"
bl_label = "Set Default Texture Path"
texture_index: bpy.props.IntProperty()
def execute(self, context):
default_texture_path = "textures\\ShaneCube.blp"
if self.texture_index == 1:
context.material.wow_m2_material.texture_1.wow_m2_texture.path = default_texture_path
elif self.texture_index == 2:
context.material.wow_m2_material.texture_2.wow_m2_texture.path = default_texture_path
return {'FINISHED'}
class TextureSlotPropertyGroup(bpy.types.PropertyGroup):
texture_flags: bpy.props.EnumProperty(
name="Texture flags",
description="WoW M2 texture flags",
items=TEXTURE_FLAGS,
options={"ENUM_FLAG"},
default={'1', '2'}
)
texture_type: bpy.props.EnumProperty(
name="Texture type",
description="WoW M2 texture type",
items=TEXTURE_TYPES
)
path: bpy.props.StringProperty(
name='Path',
description='Path to .blp file in wow file system.'
)
class ToggleTexturesOperator(bpy.types.Operator):
bl_idname = "object.toggle_textures"
bl_label = "Toggle Textures"
def execute(self, context):
context.scene.show_textures = not context.scene.show_textures
return {'FINISHED'}
class ToggleRenderFlagsOperator(bpy.types.Operator):
bl_idname = "object.toggle_render_flags"
bl_label = "Toggle Render Flags"
texture_index: bpy.props.IntProperty()
def execute(self, context):
if self.texture_index == 1:
context.scene.show_t1_render_flags = not context.scene.show_t1_render_flags
elif self.texture_index == 2:
context.scene.show_t2_render_flags = not context.scene.show_t2_render_flags
return {'FINISHED'}
bpy.types.Scene.show_textures = bpy.props.BoolProperty(name="Show Textures", default=True)
bpy.types.Scene.show_t1_render_flags = bpy.props.BoolProperty(name="Show Render Flags", default=False)
bpy.types.Scene.show_t2_render_flags = bpy.props.BoolProperty(name="Show Render Flags", default=False)
class M2_PT_Duplicate_Image(bpy.types.Operator):
bl_idname = "image.duplicate"
bl_label = "Duplicate Texture"
bl_description = "Duplicate Texture"
bl_options = {'REGISTER', 'UNDO_GROUPED'}
texture_index: bpy.props.IntProperty()
def execute(self, context):
material = context.material.wow_m2_material
if self.texture_index == 1 and material.texture_1:
new_image = material.texture_1.copy()
new_image.name = material.texture_1.name + "_copy"
material.texture_1 = new_image
elif self.texture_index == 2 and material.texture_2:
new_image = material.texture_2.copy()
new_image.name = material.texture_2.name + "_copy"
material.texture_2 = new_image
else:
self.report({'WARNING'}, "No image found to copy")
update_material_name()
return {'FINISHED'}
class M2_PT_material_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_label = "M2 Material"
def draw(self, context):
controller = util.find_color_transparency_controller()
layout = self.layout
col = layout.column()
col.label(text='Textures')
col.operator("object.toggle_textures", text="Toggle Textures")
if context.scene.show_textures:
col.separator()
col.label(text='Texture 1')
sub_col = col.column()
row = sub_col.row()
row.prop(context.material.wow_m2_material, "texture_1", text="")
op = row.operator("image.duplicate", text="", icon='IMAGE_DATA')
op.texture_index = 1
if context.material.wow_m2_material.texture_1:
col.prop(context.material.wow_m2_material.texture_1.wow_m2_texture, "flags")
col.separator()
col.prop(context.material.wow_m2_material.texture_1.wow_m2_texture, "texture_type")
# only show path setting if texture type is hardcoded
if context.material.wow_m2_material.texture_1.wow_m2_texture.texture_type == "0":
col.prop(context.material.wow_m2_material.texture_1.wow_m2_texture, "path", text='Path')
# Check if path is empty, then show the button
if len(context.material.wow_m2_material.texture_1.wow_m2_texture.path) == 0:
op = col.operator(TexturePathDefaultButton.bl_idname, text="Set Default Path", icon='FILEBROWSER')
op.texture_index = 1
col.separator()
col.label(text='Blending')
col.prop(context.material.wow_m2_material, "texture_1_blending_mode", text="")
to = col.operator("object.toggle_render_flags", text="Toggle Render Flags")
to.texture_index = 1
if context.scene.show_t1_render_flags:
col.separator()
col.label(text='Render Flags:')
box = col.box()
box.prop(context.material.wow_m2_material, "texture_1_render_flags", text="Texture 1 Render Flags", toggle=True)
col.separator()
col.prop(context.material.wow_m2_material, "texture_1_mapping")
sub_col = col.column()
row = sub_col.row()
row.prop(context.material.wow_m2_material, "texture_1_animation")
op = row.operator("scene.wow_m2_geoset_add_texture_transform", text='', icon='RNA_ADD')
op.channel = 1
col.separator()
col.label(text='Texture 2')
sub_col = col.column()
row = sub_col.row()
row.prop(context.material.wow_m2_material, "texture_2", text="")
op = row.operator("image.duplicate", text="", icon='IMAGE_DATA')
op.texture_index = 2
if context.material.wow_m2_material.texture_2:
col.prop(context.material.wow_m2_material.texture_2.wow_m2_texture, "flags")
col.separator()
col.prop(context.material.wow_m2_material.texture_2.wow_m2_texture, "texture_type")
# only show path setting if texture type is hardcoded
if context.material.wow_m2_material.texture_2.wow_m2_texture.texture_type == "0":
col.prop(context.material.wow_m2_material.texture_2.wow_m2_texture, "path", text='Path')
if len(context.material.wow_m2_material.texture_2.wow_m2_texture.path) == 0:
op = col.operator(TexturePathDefaultButton.bl_idname, text="Set Default Path", icon='FILEBROWSER')
op.texture_index = 2
col.separator()
col.label(text='Blending')
col.prop(context.material.wow_m2_material, "texture_2_blending_mode", text="")
to = col.operator("object.toggle_render_flags", text="Toggle Render Flags")
to.texture_index = 2
if context.scene.show_t2_render_flags:
col.separator()
col.label(text='Render Flags:')
box = col.box()
box.prop(context.material.wow_m2_material, "texture_2_render_flags", text="Texture 2 Render Flags", toggle=True)
col.separator()
col.prop(context.material.wow_m2_material, "texture_2_mapping")
sub_col = col.column()
row = sub_col.row()
row.prop(context.material.wow_m2_material, "texture_2_animation")
op = row.operator("scene.wow_m2_geoset_add_texture_transform", text='', icon='RNA_ADD')
op.channel = 2
col.separator()
col.label(text='Flags:')
col.prop(context.material.wow_m2_material, "flags")
col.separator()
col.label(text='Sorting control:')
col.prop(context.material.wow_m2_material, "priority_plane")
col.separator()
col.prop_search(
context.material.wow_m2_material,
"color",
controller.wow_m2_color_transparency,
"colors",
text='Color',
icon='COLOR'
)
col.prop_search(
context.material.wow_m2_material,
"transparency",
controller.wow_m2_color_transparency,
"transparencies",
text='Transparency',
icon='RESTRICT_VIEW_OFF'
)
@classmethod
def poll(cls, context):
return(context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.material is not None)
def mapping(mapping_method):
if mapping_method == "UVMap":
return 0.0
elif mapping_method == "UVMap.001":
return 1.0
elif mapping_method == "Env":
return -1.0
def update_geoset_uv_transform_1(self, context):
obj = context.object
if obj != None and obj.active_material:
c_obj = obj.active_material.wow_m2_material.texture_1_animation
tex_1_mapping = obj.active_material.wow_m2_material.texture_1_mapping
for node in obj.active_material.node_tree.nodes:
if node.name == 'Tex1_Mapping':
node.inputs[0].default_value = mapping(tex_1_mapping)
break
if c_obj:
uv_transform_1 = context.object.modifiers.get('M2TexTransform_1')
if c_obj is not None:
if c_obj.wow_m2_uv_transform is not None:
if not c_obj.wow_m2_uv_transform.enabled:
context.object.wow_m2_geoset.uv_transform = None
if not uv_transform_1:
bpy.ops.object.modifier_add(type='UV_WARP')
uv_transform_1 = context.object.modifiers[-1]
uv_transform_1.name = 'M2TexTransform_1'
uv_transform_1.object_from = obj
uv_transform_1.object_to = c_obj
uv_transform_1.uv_layer = obj.active_material.wow_m2_material.texture_1_mapping
else:
uv_transform_1.object_to = c_obj
uv_transform_1.uv_layer = obj.active_material.wow_m2_material.texture_1_mapping
else:
uv_transform_1 = context.object.modifiers.get('M2TexTransform_1')
if uv_transform_1 is not None and c_obj is None:
context.object.modifiers.remove(uv_transform_1)
def update_geoset_uv_transform_2(self, context):
obj = context.object
if obj != None and obj.active_material:
c_obj = obj.active_material.wow_m2_material.texture_2_animation
tex_2_mapping = obj.active_material.wow_m2_material.texture_2_mapping
for node in obj.active_material.node_tree.nodes:
if node.name == 'Tex2_Mapping':
node.inputs[0].default_value = mapping(tex_2_mapping)
break
if c_obj:
uv_transform_2 = context.object.modifiers.get('M2TexTransform_2')
if c_obj is not None:
if c_obj.wow_m2_uv_transform is not None:
if not c_obj.wow_m2_uv_transform.enabled:
context.object.wow_m2_geoset.uv_transform = None
if not uv_transform_2:
bpy.ops.object.modifier_add(type='UV_WARP')
uv_transform_2 = context.object.modifiers[-1]
uv_transform_2.name = 'M2TexTransform_2'
uv_transform_2.object_from = obj
uv_transform_2.object_to = c_obj
uv_transform_2.uv_layer = obj.active_material.wow_m2_material.texture_2_mapping
else:
uv_transform_2.object_to = c_obj
uv_transform_2.uv_layer = obj.active_material.wow_m2_material.texture_2_mapping
else:
uv_transform_2 = context.object.modifiers.get('M2TexTransform_2')
if uv_transform_2 is not None and c_obj is None:
context.object.modifiers.remove(uv_transform_2)
def update_material_texture(self, context):
obj = context.object
if obj != None and obj.active_material:
tex_1 = obj.active_material.wow_m2_material.texture_1
tex_2 = obj.active_material.wow_m2_material.texture_2
for node in obj.active_material.node_tree.nodes:
if node.name == 'Tex1_image':
tex1_image = node
tex1_image.image = tex_1
if node.name == 'Tex2_image':
tex2_image = node
tex2_image.image = tex_2
if node.name == 'Blending':
tex_mix = node
if node.name == 'Alpha_Blending':
tex_alpha_mix = node
if node.name == 'Color':
c_mix = node
if node.name == 'Transparency':
t_mix = node
if tex_mix and tex1_image and c_mix:
tree = obj.active_material.node_tree
links = tree.links
if tex_2:
links.new(tex_mix.outputs[2], c_mix.inputs[6])
links.new(tex_alpha_mix.outputs[2], t_mix.inputs[0])
else:
links.new(tex1_image.outputs[0], c_mix.inputs[6])
links.new(tex1_image.outputs[1], t_mix.inputs[0])
update_material_name()
BLENDING_MODES_DICT = {
"0": "Opaque",
"1": "AlphaKey",
"2": "Alpha",
"3": "NoAlphaAdd",
"4": "Add",
"5": "Mod",
"6": "Mod2X",
"7": "BlendAdd"
}
def update_material_name():
obj = bpy.context.object
if obj is not None and obj.active_material:
tex_1 = obj.active_material.wow_m2_material.texture_1
tex_2 = obj.active_material.wow_m2_material.texture_2
tex_1_name = None
tex_2_name = None
if tex_1 is not None:
tex_1_name = str(tex_1.name).replace('.png', '')
if tex_2 is not None:
tex_2_name = str(tex_2.name).replace('.png', '')
texture_1_blending_mode = obj.active_material.wow_m2_material.texture_1_blending_mode
texture_2_blending_mode = obj.active_material.wow_m2_material.texture_2_blending_mode
texture_1_blending_mode_name = BLENDING_MODES_DICT.get(str(texture_1_blending_mode), "Unknown")
texture_2_blending_mode_name = BLENDING_MODES_DICT.get(str(texture_2_blending_mode), "Unknown")
if tex_1_name:
if tex_2_name:
obj.active_material.name = 'T1_{}_({})_T2_{}_({})'.format(
tex_1_name, texture_1_blending_mode_name, tex_2_name, texture_2_blending_mode_name
)
else:
obj.active_material.name = 'T1_{}_({})'.format(
tex_1_name, texture_1_blending_mode_name
)
def update_transparency(self, context):
obj = context.object
controller = util.find_color_transparency_controller()
if obj is not None and obj.active_material:
mat = obj.active_material
transparency_node = mat.node_tree.nodes.get('Transparency')
if transparency_node:
trans_name = mat.wow_m2_material.transparency
trans_index = int(''.join(filter(str.isdigit, trans_name))) if trans_name else 0
transparency_node.label = f'Transparency_{trans_index}_OFF'
# update existing driver target to the new object property path
if mat.node_tree.animation_data:
for driver in mat.node_tree.animation_data.drivers:
if driver.data_path == 'nodes["Transparency"].inputs[1].default_value':
drv = driver.driver
for var in drv.variables:
if var.name == 'Transparency':
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = controller
var.targets[0].data_path = f"wow_m2_color_transparency.transparencies[{trans_index}].value"
transparency_node.label = f'Transparency_{trans_index}_ON'
def update_color(self, context):
obj = context.object
controller = util.find_color_transparency_controller()
if obj is not None and obj.active_material:
mat = obj.active_material
color_node = mat.node_tree.nodes.get('Color')
color_alpha_mix = mat.node_tree.nodes.get("Color_Alpha_Mix")
transparency_node = mat.node_tree.nodes.get("Transparency")
bsdf = mat.node_tree.nodes.get("BSDF")
tree = mat.node_tree
links = tree.links
color_name = mat.wow_m2_material.color
active_color = bool(color_name)
color_index = int(''.join(filter(str.isdigit, color_name))) if active_color else 0
# Update Color (RGB)
if color_node and mat.node_tree.animation_data:
for i, component in enumerate(['R', 'G', 'B']):
for driver in mat.node_tree.animation_data.drivers:
if driver.data_path.startswith('nodes["Color"].inputs[7].default_value'):
drv = driver.driver
for var in drv.variables:
if var.name == component:
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = controller
var.targets[0].data_path = f"wow_m2_color_transparency.colors[{color_index}].color[{i}]"
color_node.inputs[0].default_value = 1.0 if active_color else 0.0
color_node.label = f"Color_{color_index}_{'ON' if active_color else 'OFF'}"
# Update Alpha
if color_alpha_mix and mat.node_tree.animation_data:
for driver in mat.node_tree.animation_data.drivers:
if driver.data_path == 'nodes["Color_Alpha_Mix"].inputs[1].default_value':
drv = driver.driver
for var in drv.variables:
if var.name == "Alpha":
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = controller
var.targets[0].data_path = f"wow_m2_color_transparency.colors[{color_index}].alpha"
# Node linking
if transparency_node and bsdf:
if active_color:
color_alpha_mix.label = f'Color_{color_index}_Alpha_ON'
links.new(color_alpha_mix.inputs['Value'], transparency_node.outputs['Value'])
links.new(color_alpha_mix.outputs['Value'], bsdf.inputs['Alpha'])
else:
color_alpha_mix.label = f'Color_{color_index}_Alpha_OFF'
links.new(transparency_node.outputs['Value'], bsdf.inputs['Alpha'])
def update_blending(self, context):
obj = context.object
if obj != None and obj.active_material and obj.active_material.wow_m2_material:
blending_1 = int(obj.active_material.wow_m2_material.texture_1_blending_mode)
blending_2 = int(obj.active_material.wow_m2_material.texture_2_blending_mode)
for node in obj.active_material.node_tree.nodes:
if node.name == 'Tex1_image':
tex1_image = node
if node.name == 'Blending':
tex_mix = node
if node.name == 'Color':
c_mix = node
if tex_mix and tex1_image and c_mix:
tree = obj.active_material.node_tree
links = tree.links
tex_2 = obj.active_material.wow_m2_material.texture_2
if tex_2:
links.new(tex_mix.outputs[2], c_mix.inputs[6])
else:
links.new(tex1_image.outputs[0], c_mix.inputs[6])
if blending_2 in [1, 2]:
tex_mix.blend_type = 'MIX'
elif blending_2 == 4:
tex_mix.blend_type = 'OVERLAY'
elif blending_2 == 5:
tex_mix.blend_type = 'MULTIPLY'
# Alpha_mode = obj.active_material.node_tree.nodes.get('Tex1_image')
# if blending_1 in [1, 2, 4, 5, 6]:
# Alpha_mode.image.alpha_mode = 'CHANNEL_PACKED'
# else:
# Alpha_mode.image.alpha_mode = 'NONE'
update_material_name()
class WowM2MaterialPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty()
flags: bpy.props.EnumProperty(
name="Material flags",
description="WoW M2 material flags",
items=TEX_UNIT_FLAGS,
options={"ENUM_FLAG"}
)
texture_1_render_flags: bpy.props.EnumProperty(
name="Render flags",
description="WoW M2 render flags",
items=RENDER_FLAGS,
options={"ENUM_FLAG"}
)
texture_1_animation: bpy.props.PointerProperty(
name="UV Transform",
description="WoW M2 texture 1 animation",
type=bpy.types.Object,
poll=lambda self, obj: obj.wow_m2_uv_transform.enabled,
update=update_geoset_uv_transform_1
)
texture_2_animation: bpy.props.PointerProperty(
name="UV Transform",
description="WoW M2 texture 2 animation",
type=bpy.types.Object,
poll=lambda self, obj: obj.wow_m2_uv_transform.enabled,
update=update_geoset_uv_transform_2
)
texture_2_render_flags: bpy.props.EnumProperty(
name="Render flags",
description="WoW M2 render flags",
items=RENDER_FLAGS,
options={"ENUM_FLAG"}
)
vertex_shader: bpy.props.EnumProperty(
items=VERTEX_SHADERS,
name="Vertex Shader",
description="WoW vertex shader assigned to this material",
default='0'
)
fragment_shader: bpy.props.EnumProperty(
items=FRAGMENT_SHADERS,
name="Fragment Shader",
description="WoW fragment shader assigned to this material",
default='0'
)
shader: bpy.props.IntProperty(
name='Shader'
)
texture_1_blending_mode: bpy.props.EnumProperty(
items=BLENDING_MODES,
name="Blending",
description="WoW material blending mode",
update=update_blending
)
texture_2_blending_mode: bpy.props.EnumProperty(
items=BLENDING_MODES,
name="Blending",
description="WoW material blending mode",
update=update_blending
)
texture_1_mapping: bpy.props.EnumProperty(
items=TEXTURE_MAPPING,
name="Mapping",
description="Select the mapping for Texture 1",
default='UVMap',
update=update_geoset_uv_transform_1
)
texture_2_mapping: bpy.props.EnumProperty(
items=TEXTURE_MAPPING,
name="Mapping",
description="Select the mapping for Texture 2",
default='UVMap.001',
update=update_geoset_uv_transform_2
)
texture_1: bpy.props.PointerProperty(
type=bpy.types.Image,
update=update_material_texture
)
texture_2: bpy.props.PointerProperty(
type=bpy.types.Image,
update=update_material_texture
)
#Removed layer, we can calculate it on export by material index
# layer: bpy.props.IntProperty(
# min=0,
# max=7
# )
priority_plane: bpy.props.IntProperty(
min=-127,
max=127,
default=0
)
color: bpy.props.StringProperty(
name='Color',
description='Color track linked to this texture.',
update=update_color
)
transparency: bpy.props.StringProperty(
name='Transparency',
description='Transparency track linked to this texture.',
update=update_transparency
)
self_pointer: bpy.props.PointerProperty(type=bpy.types.Material)
def get_animations(self, context):
global_seqs = []
for i, anim in enumerate(context.scene.wow_m2_animations):
if anim.is_global_sequence:
identifier = str(i)
name = anim.name
description = f"Global sequence {i}"
global_seqs.append((identifier, name, description))
return global_seqs
class M2_OT_add_texture_transform(bpy.types.Operator):
bl_idname = 'scene.wow_m2_geoset_add_texture_transform'
bl_label = 'Add Texture Animation (UV) Controller'
bl_description = 'Add an M2 TT_Controller object to the scene'
bl_options = {'REGISTER', 'UNDO_GROUPED', 'INTERNAL'}
anim_index: bpy.props.IntProperty()
channel: bpy.props.IntProperty(min=1, max=2)
frame_end: bpy.props.IntProperty(
name="Final frame",
default=100,
min=1
)
x_value: bpy.props.IntProperty(
description='Final Texture Transform value, -1 or 1 = full UV loop',
default=1,
min = -10,
max = 10,
)
y_value: bpy.props.IntProperty(
description='Final Texture Transform value, -1 or 1 = full UV loop',
default=1,
min = -10,
max = 10,
)
data_path: bpy.props.EnumProperty(
description='Choose type of animation',
items=[('location', 'Location', 'Location'),
('rotation_quaternion', 'Rotation', 'Rotation'),
('scale', 'Scale', 'Scale')],
default='location'
)
sequence: bpy.props.EnumProperty(
description='Choose type of animation',
items=get_animations
)
def draw(self, context):
layout = self.layout
layout.prop(self, "data_path", text="L/R/S")
layout.prop(self, "x_value", text="X Value")
layout.prop(self, "y_value", text="Y Value")
layout.prop(self, "frame_end", text="Final Frame")
layout.prop(self, "sequence", text="Choose Global Sequence")
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
mat_obj = context.object
#Create TT_Controller
bpy.ops.object.empty_add(type='SINGLE_ARROW', location=(0, 0, 0))
TT_Controllers = [obj for obj in bpy.data.objects if obj.wow_m2_uv_transform.enabled]
obj = bpy.context.view_layer.objects.active
obj.name = "TT_Controller_{}".format(len(TT_Controllers))
obj.wow_m2_uv_transform.enabled = True
obj.rotation_mode = 'QUATERNION'
obj.empty_display_size = 0.5
#Animate TT_Controller
obj.animation_data_create()
obj.animation_data.action_blend_type = 'ADD'
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.view_layer.objects.active = obj
action_name = ('TT_{}_{}_Custom'.format(len(TT_Controllers), obj.name))
action = bpy.data.actions.new(name=action_name)
obj.animation_data.action = action
frame_start = 0
frame_end = self.frame_end
fcurve_x = action.fcurves.new(data_path=self.data_path, index=0, action_group=obj.name)
fcurve_y = action.fcurves.new(data_path=self.data_path, index=1, action_group=obj.name)
fcurve_z = action.fcurves.new(data_path=self.data_path, index=2, action_group=obj.name)
fcurve_x.keyframe_points.insert(frame_start, 0)
fcurve_y.keyframe_points.insert(frame_start, 0)
fcurve_z.keyframe_points.insert(frame_start, 0)
fcurve_x.keyframe_points.insert(frame_end, self.x_value)
fcurve_y.keyframe_points.insert(frame_end, self.y_value)
fcurve_z.keyframe_points.insert(frame_end, 0)
for fcurve in [fcurve_x, fcurve_y, fcurve_z]:
for keyframe_point in fcurve.keyframe_points:
keyframe_point.interpolation = 'LINEAR'
#Add TT_Controller to Global Sequence
index = context.scene.wow_m2_cur_anim_index = int(self.sequence)
pairs = 0
for pair in bpy.data.scenes["Scene"].wow_m2_animations[index].anim_pairs:
pairs += 1
bpy.ops.scene.wow_m2_animation_editor_object_add()
bpy.data.scenes["Scene"].wow_m2_animations[index].anim_pairs[pairs].object = obj
bpy.data.scenes["Scene"].wow_m2_animations[index].anim_pairs[pairs].action = action
#Add TT_Controller to material
if self.channel == 1:
mat_obj.active_material.wow_m2_material.texture_1_animation = obj
else:
mat_obj.active_material.wow_m2_material.texture_2_animation = obj
bpy.context.view_layer.objects.active = mat_obj
self.report({'INFO'}, "Successfully created M2 Texture Animation: " + obj.name + "\n")
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="Info: Successfully created M2 Texture Animation", font_size=24, y_offset=67)
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message=f"If you want to edit it, select: {obj.name} along with this action: {action_name}", font_size=16, y_offset=100)
return {'FINISHED'}
def menu_func(self, context):
self.layout.operator(M2_OT_add_texture_transform.bl_idname)
def register():
bpy.utils.register_class(TexturePathDefaultButton)
bpy.types.Material.wow_m2_material = bpy.props.PointerProperty(type=WowM2MaterialPropertyGroup)
def unregister():
bpy.utils.unregister_class(TexturePathDefaultButton)
del bpy.types.Material.wow_m2_material
@@ -0,0 +1,470 @@
import bpy
from ..enums import *
class TexturePathDefaultButton(bpy.types.Operator):
bl_idname = "wow_m2_texture.set_default_texture"
bl_label = "Set Default Texture Path"
def execute(self, context):
default_texture_path = "textures\\ShaneCube.blp"
context.object.wow_m2_particle.texture.wow_m2_texture.path = default_texture_path
return {'FINISHED'}
class ToggleFlagsOperator(bpy.types.Operator):
bl_idname = "particles.toggle_flags"
bl_label = "Toggle Particle Flags"
def execute(self, context):
context.scene.show_flags = not context.scene.show_flags
return {'FINISHED'}
bpy.types.Scene.show_flags = bpy.props.BoolProperty(name="Toggle Particle Flags", default=False)
class M2_PT_particle_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "M2 Particle"
def draw_header(self, context):
self.layout.prop(context.object.wow_m2_particle, "enabled", text="")
def draw(self, context):
layout = self.layout
col = layout.column()
particle = context.object.wow_m2_particle
col.operator("particles.toggle_flags", text="Toggle Particle Flags")
if context.scene.show_flags:
col.prop(particle, 'flags', text='Flags')
col.prop(particle, 'action',text='Action')
col.prop(particle, 'texture',text='Texture')
try:
col.prop(particle.texture.wow_m2_texture, "path", text='Path')
if len(particle.texture.wow_m2_texture.path) == 0:
col.operator(TexturePathDefaultButton.bl_idname, text="Set Default Path", icon='FILEBROWSER')
except:
pass
col.prop(particle, 'geometry_model_filename', text='Geometry Model Filename')
col.prop(particle, 'recursion_model_filename', text='Recursion Model Filename')
col.prop(particle, 'blending_type',text="Blending Type")
col.prop(particle, 'emitter_type',text="Alpha")
col.prop(particle, 'particle_type',text="Particle Type")
col.prop(particle, 'side',text="Side")
col.prop(particle, 'particle_color_index',text="Particle Color Index")
col.prop(particle, 'texture_tile_rotation',text="Texture Tile Rotation")
col.prop(particle, 'texture_dimensions_rows',text="Texture Dimension Rows")
col.prop(particle, 'texture_dimensions_cols',text="Texture Dimension Columns")
col.prop(particle, 'emission_speed',text="Emission Speed")
col.prop(particle, 'speed_variation',text="Speed Variation")
col.prop(particle, 'vertical_range',text="Vertical Range")
col.prop(particle, 'horizontal_range',text="Horizontal Range")
col.prop(particle, 'gravity',text="Gravity")
col.prop(particle, 'lifespan',text="Lifespan")
col.prop(particle, 'lifespan_vary',text="Lifespan Vary")
col.prop(particle, 'emission_rate',text="Emission Rate")
col.prop(particle, 'emission_rate_vary',text="Emission Rate Vary")
col.prop(particle, 'emission_area_length',text="Emission Area Length")
col.prop(particle, 'emission_area_width',text="Emission Area Width")
col.prop(particle, 'z_source',text="Z Source")
col.prop(particle, 'color',text="Color")
col.prop(particle, 'alpha',text="Alpha")
col.prop(particle, 'scale',text="Scale")
col.prop(particle, 'scale_vary',text="Scale Vary")
col.prop(particle, 'head_cell',text="Head Cell")
col.prop(particle, 'tail_cell',text="Tail Cell")
col.prop(particle, 'tail_length',text="Tail Length")
col.prop(particle, 'twinkle_speed',text="Twinkle Speed")
col.prop(particle, 'twinkle_percent',text="Twinkle Percent")
col.prop(particle, 'twinkle_scale',text="Twinkle Scale")
col.prop(particle, 'burst_multiplier',text="Burst Multiplier")
col.prop(particle, 'drag',text="Drag")
col.prop(particle, 'basespin',text="Base Spin")
col.prop(particle, 'basespin_vary',text="Base Spin Vary")
col.prop(particle, 'spin',text="Spin")
col.prop(particle, 'spin_vary',text="Spin Vary")
col.prop(particle, 'tumble_min',text="Tumble Min")
col.prop(particle, 'tumble_max',text="Tumble Max")
col.prop(particle, 'wind',text="Wind")
col.prop(particle, 'wind_time',text="Wind Time")
col.prop(particle, 'follow_speed_1',text="Follow Speed #1")
col.prop(particle, 'follow_scale_1',text="Follow Scale #1")
col.prop(particle, 'follow_speed_2',text="Follow Speed #2")
col.prop(particle, 'follow_scale_2',text="Follow Scale #2")
col.prop(particle, 'spline_action',text="Spline Action")
col.prop(particle, 'spline_point',text="Spline Point")
col.prop(particle, 'active',text="Active")
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.object is not None
and context.object.type == 'EMPTY'
and not (context.object.wow_m2_event.enabled
or context.object.wow_m2_uv_transform.enabled
or context.object.wow_m2_camera.enabled
or context.object.wow_m2_attachment.enabled
or context.object.wow_m2_ribbon.enabled
)
)
class WowM2ParticlePropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name='Enabled',
description='Enabled this object to be a WoW M2 Particle',
default=False
)
action: bpy.props.PointerProperty(
name='Action',
description='',
type=bpy.types.Action
)
flags: bpy.props.EnumProperty(
name="Material flags",
description="",
items=PARTICLE_FLAGS,
options={"ENUM_FLAG"}
)
texture: bpy.props.PointerProperty (
type=bpy.types.Image
)
geometry_model_filename: bpy.props.StringProperty (
name = 'Geometry Model Filename',
description = '',
default = ''
)
recursion_model_filename: bpy.props.StringProperty (
name = 'Recursion Model Filename',
description = '',
default = ''
)
blending_type: bpy.props.EnumProperty (
name='Blending Type',
description='',
items = [
('0','0: glDisable(GL_BLEND); glDisable(GL_ALPHA_TEST)',''),
('1','1: glBlendFunc(GL_SRC_COLOR, GL_ONE)',''),
('2','2: glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)',''),
('3','3: glDisable(GL_BLEND); glEnable(GL_ALPHA_TEST)',''),
('4','4: glBlendFunc(GL_SRC_ALPHA, GL_ONE)',''),
]
)
emitter_type: bpy.props.EnumProperty(
name='Emitter Type',
description='The shape of this emitter',
items = [
('0','Rectangle',''),
('1','Sphere',''),
('2','Spline',''),
('3','Bone',''),
]
)
particle_color_index: bpy.props.IntProperty(
name='Particle Color Index',
description='An index into ParticleColor.dbc',
default = 0
)
particle_type: bpy.props.EnumProperty(
name='Particle Type',
description='',
items = [
('0','Normal','Normal particle, usual way to go'),
('1','Large Quad','Used in Moonwell water effects'),
('2','Unknown','Used in Deeprun Tram'),
]
)
side: bpy.props.EnumProperty(
name='Side',
description='What sides of the particle emits',
items = [
('0','Head',''),
('1','Tail',''),
('2','Both',''),
]
)
texture_tile_rotation: bpy.props.IntProperty(
name='Texture Tile Rotation',
description='',
min=-1,
max=1,
default=0
)
texture_dimensions_rows: bpy.props.IntProperty(
name='Texture Dimensions Rows',
description='Used to divide the used texture in rows',
default=1
)
texture_dimensions_cols: bpy.props.IntProperty(
name='Texture Dimensions Columns',
description='Used to divide the used texture in columns',
default=1
)
emission_speed: bpy.props.FloatProperty(
name='Emission Speed',
description='',
default=0.0
)
speed_variation: bpy.props.FloatProperty(
name='Speed Variation',
description='',
default=0.0
)
vertical_range: bpy.props.FloatProperty(
name='Speed Variation',
description='',
default=0.0
)
horizontal_range: bpy.props.FloatProperty(
name='Horizontal Range',
description='',
default=0.0
)
gravity: bpy.props.FloatProperty(
name='Gravity',
description='',
default=0.0
)
lifespan: bpy.props.FloatProperty(
name='Lifespan',
description='',
default=0.0
)
lifespan_vary: bpy.props.FloatProperty(
name='Lifespan Vary',
description='',
default=0.0
)
emission_rate: bpy.props.FloatProperty(
name='Emission Rate',
description='',
default=0.0
)
emission_rate_vary: bpy.props.FloatProperty(
name='Emission Rate Vary',
description='',
default=0.0
)
emission_area_length: bpy.props.FloatProperty(
name='Emission Area Length',
description='',
default=0.0
)
emission_area_width: bpy.props.FloatProperty(
name='Emission Area Width',
description='',
default=0.0
)
z_source: bpy.props.FloatProperty(
name='Z Source',
description='',
default=0.0
)
color: bpy.props.FloatVectorProperty(
name = "Color",
description="",
subtype='COLOR',
size=3,
default=(1.0,1.0,1.0),
min=0.0,
max=1.0
)
alpha: bpy.props.FloatProperty(
name = "Alpha",
description="",
default=0,
min=0,
max=1
)
scale: bpy.props.FloatVectorProperty(
name = "Scale",
description="",
size=2,
default=(1.0,1.0),
)
scale_vary: bpy.props.FloatVectorProperty(
name = "Scale Vary",
description="",
size=2,
default=(1.0,1.0),
)
head_cell: bpy.props.IntProperty(
name = "Head Cell",
description = "",
default = 0
)
tail_cell: bpy.props.IntProperty(
name = "Tail Cell",
description = "",
default = 0
)
tail_length: bpy.props.FloatProperty(
name = "Tail Length",
description = "",
default = 0
)
twinkle_speed: bpy.props.FloatProperty(
name = "Twinkle Speed",
description = "",
default = 0
)
twinkle_percent: bpy.props.FloatProperty(
name = "Twinkle Percent",
description = "",
default = 0
)
twinkle_scale: bpy.props.FloatVectorProperty(
name = "Twinkle Scale",
description = "",
size=2,
default=(1.0,1.0)
)
burst_multiplier: bpy.props.FloatProperty(
name = "Burst Multiplier",
description = "",
default = 1
)
drag: bpy.props.FloatProperty(
name = "Drag",
description = "",
default = 1
)
basespin: bpy.props.FloatProperty(
name = "Base Spin",
description = "",
default = 0
)
basespin_vary: bpy.props.FloatProperty(
name = "Base Spin Vary",
description = "",
default = 0
)
spin: bpy.props.FloatProperty(
name = "Spin",
description = "",
default = 0
)
spin_vary: bpy.props.FloatProperty(
name = "Spin Vary",
description = "",
default = 0
)
tumble_min: bpy.props.FloatVectorProperty(
name = "Tumble Min",
description = "",
size=3,
default = (0.0,0.0,0.0),
)
tumble_max: bpy.props.FloatVectorProperty(
name = "Tumble Max",
description = "",
size=3,
default = (0.0,0.0,0.0),
)
wind: bpy.props.FloatVectorProperty(
name = "Wind",
description = "",
size=3,
default = (0.0,0.0,0.0)
)
wind_time: bpy.props.FloatProperty(
name = "Wind Time",
description = "",
default = 0
)
follow_speed_1: bpy.props.FloatProperty(
name = "Follow Speed #1",
description = "",
default = 0
)
follow_scale_1: bpy.props.FloatProperty(
name = "Follow Scale #1",
description = "",
default = 0
)
follow_speed_2: bpy.props.FloatProperty(
name = "Follow Speed #2",
description = "",
default = 0
)
follow_scale_2: bpy.props.FloatProperty(
name = "Follow Scale #2",
description = "",
default = 0
)
spline_action: bpy.props.PointerProperty(
name = 'Spline Action',
description = 'FCurve describing the spline point values in this particle. Important: timestamps are only used for ordering, time values are discarded on export.',
type = bpy.types.Action
)
spline_point: bpy.props.FloatVectorProperty(
name = 'Spline Point',
description = '',
size=3,
default = (0.0,0.0,0.0)
)
active: bpy.props.BoolProperty(
name = "Active",
description = "",
default = False
)
def register():
bpy.types.Object.wow_m2_particle = bpy.props.PointerProperty(type=WowM2ParticlePropertyGroup)
def unregister():
del bpy.types.Object.wow_m2_particle
@@ -0,0 +1,172 @@
import bpy
from ..enums import *
from .utils import draw_object_list
class RibbonMaterialPointerPropertyGroup(bpy.types.PropertyGroup):
pointer: bpy.props.PointerProperty(
name='Material',
type=bpy.types.Material,
)
class RibbonTexturePointerPropertyGroup(bpy.types.PropertyGroup):
pointer: bpy.props.PointerProperty(
name='Texture',
type=bpy.types.Image,
)
class M2_PT_ribbon_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "M2 Ribbon"
def draw_header(self, context):
self.layout.prop(context.object.wow_m2_ribbon, "enabled", text="")
def draw(self, context):
layout = self.layout
layout.enabled = context.object.wow_m2_ribbon.enabled
col = layout.column()
draw_object_list(
context.object.wow_m2_ribbon,col,
'Textures',
'M2_UL_root_elements_textures_list',
'wow_m2_ribbon',
'textures',
'cur_texture'
)
draw_object_list(
context.object.wow_m2_ribbon,col,
'Materials',
'M2_UL_root_elements_materials_list',
'wow_m2_ribbon',
'materials',
'cur_material'
)
col.prop(context.object.wow_m2_ribbon, 'color', text="Color")
col.prop(context.object.wow_m2_ribbon, 'alpha', text="Alpha")
col.prop(context.object.wow_m2_ribbon, 'height_above', text="Height Above")
col.prop(context.object.wow_m2_ribbon, 'height_below', text="Height Below")
col.prop(context.object.wow_m2_ribbon, 'texture_slot', text="Texture Slot")
col.prop(context.object.wow_m2_ribbon, 'edges_per_second', text="Edges Per Second")
col.prop(context.object.wow_m2_ribbon, 'edge_lifetime', text="Edges Lifetime")
col.prop(context.object.wow_m2_ribbon, 'gravity', text="Gravity")
col.prop(context.object.wow_m2_ribbon, 'texture_rows', text="Texture Rows")
col.prop(context.object.wow_m2_ribbon, 'texture_cols', text="Texture Cols")
col.prop(context.object.wow_m2_ribbon, 'priority_plane', text="Priority Plane")
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.object is not None
and context.object.type == 'EMPTY'
and not (context.object.wow_m2_event.enabled
or context.object.wow_m2_uv_transform.enabled
or context.object.wow_m2_camera.enabled
or context.object.wow_m2_attachment.enabled
or context.object.wow_m2_particle.enabled
)
)
class WowM2RibbonPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name='Enabled',
description='Enabled this object to be a WoW M2 Ribbon',
default=False
)
textures: bpy.props.CollectionProperty(type=RibbonTexturePointerPropertyGroup)
cur_texture: bpy.props.IntProperty()
materials: bpy.props.CollectionProperty(type=RibbonMaterialPointerPropertyGroup)
cur_material: bpy.props.IntProperty()
color: bpy.props.FloatVectorProperty(
name = "Color",
description="",
subtype='COLOR',
size=3,
default=(1.0,1.0,1.0),
min=0.0,
max=1.0
)
alpha: bpy.props.FloatProperty(
name = "Alpha",
description="",
default=0,
min=0,
max=1
)
height_above: bpy.props.FloatProperty(
name = "Height Above",
description="",
default=0
)
height_below: bpy.props.FloatProperty(
name = "Height Below",
description="",
default=0
)
texture_slot: bpy.props.IntProperty(
name = "Texture Slot",
description = "",
default = 0
)
visibility: bpy.props.IntProperty(
name = "Visibility",
description = "",
default = 0
)
edges_per_second: bpy.props.FloatProperty(
name = "Edges Per Second",
description = "",
default = 0
)
edge_lifetime: bpy.props.FloatProperty(
name = "Edges Lifetime",
description = "",
default = 0
)
gravity: bpy.props.FloatProperty(
name = "Gravity",
description = "",
default = 0
)
texture_rows: bpy.props.IntProperty(
name = "Texture Rows",
description = "",
default = 0
)
texture_cols: bpy.props.IntProperty(
name = "Texture Cols",
description = "",
default = 0
)
priority_plane: bpy.props.IntProperty(
name = "Priority Plane",
description = "",
default = 0
)
def register():
bpy.types.Object.wow_m2_ribbon = bpy.props.PointerProperty(type=WowM2RibbonPropertyGroup)
def unregister():
del bpy.types.Object.wow_m2_ribbon
@@ -0,0 +1,523 @@
from collections import namedtuple
import bpy
from .light import M2_PT_light_panel
from .material import M2_PT_material_panel
from .geoset import M2_PT_geoset_panel
from .attachment import M2_PT_attachment_panel
from .event import M2_PT_event_panel
from .color_transparency import *
from .texture import M2_PT_texture_panel
from .utils import M2_UL_root_elements_template_list, update_current_object, is_obj_unused
from .... import ui_icons
######################
###### UI Lists ######
######################
class M2_UL_root_elements_groups_list(M2_UL_root_elements_template_list, bpy.types.UIList):
icon = 'FILE_3D'
class M2_UL_root_elements_attachments_list(M2_UL_root_elements_template_list, bpy.types.UIList):
icon = 'POSE_HLT'
class M2_UL_root_elements_events_list(M2_UL_root_elements_template_list, bpy.types.UIList):
icon = 'PLUGIN'
class M2_UL_root_elements_materials_list(M2_UL_root_elements_template_list, bpy.types.UIList):
icon = 'MATERIAL_DYNAMIC'
class M2_UL_root_elements_textures_list(M2_UL_root_elements_template_list, bpy.types.UIList):
icon = 'IMAGE_DATA'
class M2_UL_root_elements_lights_list(M2_UL_root_elements_template_list, bpy.types.UIList):
icon = 'LIGHT'
_ui_lists = {
'geosets': 'M2_UL_root_elements_groups_list',
'attachments': 'M2_UL_root_elements_attachments_list',
'events': 'M2_UL_root_elements_events_list',
'materials': 'M2_UL_root_elements_materials_list',
'lights': 'M2_UL_root_elements_lights_list',
'textures': 'M2_UL_root_elements_textures_list',
}
#####################
##### Panels #####
#####################
m2_widget_items = (
("GEOSETS", "", "M2 Geosets", 'FILE_3D', 0),
("ATTACHMENTS", "", "M2 Attachments", 'POSE_HLT', 1),
("MATERIALS", "", "M2 Materials", 'MATERIAL', 2),
("EVENTS", "", "M2 Events", 'PLUGIN', 3),
("LIGHTS", "", "M2 Lights",'LIGHT', 4),
("TEXTURES", "", "M2 Textures", 'IMAGE_DATA', 5)
)
m2_widget_labels = {item[0] : item[2] for item in m2_widget_items}
# class M2_PT_root_elements(bpy.types.Panel):
# bl_space_type = "PROPERTIES"
# bl_region_type = "WINDOW"
# bl_context = "scene"
# bl_label = "M2 Components"
# @classmethod
# def poll(cls, context):
# return (context.scene is not None
# and context.scene.wow_scene.type == 'M2')
# def draw(self, context):
# layout = self.layout
#row = layout.row(align=True)
#row.prop(context.scene.wow_m2_root_elements, 'cur_widget', expand=True)
# row.label(text=m2_widget_labels[context.scene.wow_m2_root_elements.cur_widget])
# col = layout.column()
# cur_widget = context.scene.wow_m2_root_elements.cur_widget
# if cur_widget == 'GEOSETS':
# draw_m2_geosets_panel(col, context)
# elif cur_widget == 'MATERIALS':
# draw_m2_materials_panel(col, context)
# elif cur_widget == 'LIGHTS':
# draw_m2_lights_panel(col, context)
# elif cur_widget == 'TEXTURES':
# draw_m2_textures_panel(col, context)
# elif cur_widget == 'ATTACHMENTS':
# draw_m2_attachments_panel(col, context)
# elif cur_widget == 'EVENTS':
# draw_m2_events_panel(col, context)
# else:
# pass # invalid identifier
def draw_m2_geosets_panel(layout, context):
layout = draw_list(context, layout, 'cur_geoset', 'geosets')
root_comps = context.scene.wow_m2_root_elements
geosets = root_comps.geosets
cur_geoset = root_comps.cur_geoset
ctx_override = namedtuple('ctx_override', ('object', 'scene', 'layout'))
# print(geosets)
# print(len(geosets))
# print(cur_geoset)
if len(geosets) > cur_geoset:
obj = geosets[cur_geoset].pointer
if obj:
# print("passed") # can reach here
box = layout.box()
box.label(text='Properties', icon='PREFERENCES')
ctx = ctx_override(obj, context.scene, box)
M2_PT_geoset_panel.draw(ctx, ctx)
def draw_m2_lights_panel(layout, context):
layout = draw_list(context, layout, 'cur_light', 'lights')
root_comps = context.scene.wow_m2_root_elements
lights = root_comps.lights
cur_light = root_comps.cur_light
ctx_override = namedtuple('ctx_override', ('object', 'scene', 'layout'))
if len(lights) > cur_light:
obj = lights[cur_light].pointer
if obj:
box = layout.box()
box.label(text='Properties', icon='PREFERENCES')
ctx = ctx_override(obj, context.scene, box)
M2_PT_light_panel.draw(ctx, ctx)
def draw_m2_events_panel(layout, context):
layout = draw_list(context, layout, 'cur_event', 'events')
root_comps = context.scene.wow_m2_root_elements
events = root_comps.events
cur_event = root_comps.cur_event
ctx_override = namedtuple('ctx_override', ('object', 'scene', 'layout'))
if len(events) > cur_event:
obj = events[cur_event].pointer
if obj:
box = layout.box()
box.label(text='Properties', icon='PREFERENCES')
ctx = ctx_override(obj, context.scene, box)
M2_PT_event_panel.draw(ctx, ctx)
def draw_m2_attachments_panel(layout, context):
layout = draw_list(context, layout, 'cur_attachment', 'attachments')
root_comps = context.scene.wow_m2_root_elements
attachments = root_comps.attachments
cur_attachment = root_comps.cur_attachment
ctx_override = namedtuple('ctx_override', ('object', 'scene', 'layout'))
if len(attachments) > cur_attachment:
obj = attachments[cur_attachment].pointer
if obj:
box = layout.box()
box.label(text='Properties', icon='PREFERENCES')
ctx = ctx_override(obj, context.scene, box)
M2_PT_attachment_panel.draw(ctx, ctx)
def draw_m2_materials_panel(layout, context):
layout = draw_list(context, layout, 'cur_material', 'materials')
if bpy.context.view_layer.objects.active and bpy.context.view_layer.objects.active.mode == 'EDIT':
row = layout.row(align=True)
row.operator("object.wow_m2_material_assign", text="Assign")
row.operator("object.wow_m2_material_select", text="Select")
row.operator("object.wow_m2_material_deselect", text="Deselect")
root_comps = context.scene.wow_m2_root_elements
materials = root_comps.materials
cur_material = root_comps.cur_material
ctx_override = namedtuple('ctx_override', ('material', 'scene', 'layout'))
if len(materials) > cur_material:
mat = materials[cur_material].pointer
if mat:
box = layout.box()
box.label(text='Properties', icon='PREFERENCES')
ctx = ctx_override(mat, context.scene, box)
M2_PT_material_panel.draw(ctx, ctx)
def draw_m2_textures_panel(layout, context):
layout = draw_list(context, layout, 'cur_texture', 'textures')
if bpy.context.view_layer.objects.active and bpy.context.view_layer.objects.active.mode == 'EDIT':
row = layout.row(align=True)
row.operator("object.wow_m2_texture_assign", text="Assign")
row.operator("object.wow_m2_texture_select", text="Select")
row.operator("object.wow_m2_texture_deselect", text="Deselect")
root_comps = context.scene.wow_m2_root_elements
textures = root_comps.textures
cur_texture = root_comps.cur_texture
ctx_override = namedtuple('ctx_override', ('edit_image', 'scene', 'layout'))
if len(textures) > cur_texture:
text = textures[cur_texture].pointer
if text:
box = layout.box()
box.label(text='Properties', icon='PREFERENCES')
ctx = ctx_override(text, context.scene, box)
M2_PT_texture_panel.draw(ctx, ctx)
def draw_list(context, col, cur_idx_name, col_name):
row = col.row()
sub_col1 = row.column()
sub_col1.template_list(_ui_lists[col_name], "",
context.scene.wow_m2_root_elements,
col_name, context.scene.wow_m2_root_elements, cur_idx_name)
sub_col_parent = row.column()
sub_col2 = sub_col_parent.column(align=True)
if col_name not in():
op = sub_col2.operator("scene.wow_m2_root_elements_change", text='', icon='ADD')
op.action, op.add_action, op.col_name, op.cur_idx_name = 'ADD', 'NEW', col_name, cur_idx_name
if col_name not in ('attachments', 'events'):
op = sub_col2.operator("scene.wow_m2_root_elements_change", text='', icon='COLLECTION_NEW')
op.action, op.add_action, op.col_name, op.cur_idx_name = 'ADD', 'EMPTY', col_name, cur_idx_name
op = sub_col2.operator("scene.wow_m2_root_elements_change", text='', icon='REMOVE')
op.action, op.col_name, op.cur_idx_name = 'REMOVE', col_name, cur_idx_name
return sub_col1
###########################
##### Property Groups #####
###########################
prop_map = {
'wow_m2_geoset': 'geosets',
'wow_m2_attachment' : 'attachments',
'wow_m2_material': 'materials',
'wow_m2_event' : 'events',
'wow_m2_light': 'lights',
'wow_m2_texture': 'texture',
}
def update_object_pointer(self, context, prop, obj_type):
if self.pointer:
# handle replacing pointer value
if self.pointer_old:
if getattr(context.scene.wow_m2_root_elements, prop_map[prop]).find(self.pointer.name) < 0:
getattr(self.pointer_old, prop).enabled = False
self.pointer_old = None
# check if object is another type
if not is_obj_unused(self.pointer) \
or self.pointer.type != obj_type \
or getattr(context.scene.wow_m2_root_elements, prop_map[prop]).find(self.pointer.name) >= 0:
# TODO : disabled this because it fucks up events/attachments pointers
# self.pointer = None
return
# print("updating object pointer")
print(self.pointer)
print(type(self.pointer))
getattr(self.pointer, prop).enabled = True
self.pointer_old = self.pointer
self.name = self.pointer.name
elif self.pointer_old:
# handle deletion
getattr(self.pointer_old, prop).enabled = False
self.pointer_old = None
self.name = ""
def update_geoset_pointer(self, context):
update_object_pointer(self, context, 'wow_m2_geoset', 'MESH')
# force pass index recalculation
if self.pointer:
act_obj = context.view_layer.objects.active
context.view_layer.objects.active = self.pointer
self.pointer.wow_m2_geoset.mesh_part_group = self.pointer.wow_m2_geoset.mesh_part_group
self.pointer.wow_m2_geoset.mesh_part_id = self.pointer.wow_m2_geoset.mesh_part_id
context.view_layer.objects.active = act_obj
def update_material_pointer(self, context):
if self.pointer:
# handle replacing pointer value
if self.pointer_old\
and context.scene.wow_m2_root_elements.materials.find(self.pointer.name) < 0:
self.pointer_old.wow_m2_material.enabled = False
self.pointer_old = None
# check if material is used
if self.pointer.wow_m2_material.enabled \
and context.scene.wow_m2_root_elements.materials.find(self.pointer.name) >= 0:
self.pointer = None # !! BUG HAPPENS THERE, it removes elements from their slots
return
self.pointer.wow_m2_material.enabled = True
self.pointer.wow_m2_material.self_pointer = self.pointer
self.pointer_old = self.pointer
self.name = self.pointer.name
# force pass index recalculation
ctx_override = namedtuple('ctx_override', ('material',))
ctx = ctx_override(self.pointer)
# update_shader(self.pointer.wow_m2_material, ctx)
# update_flags(self.pointer.wow_m2_material, ctx)
elif self.pointer_old:
# handle deletion
self.pointer_old.wow_m2_material.enabled = False
self.pointer_old = None
self.name = ""
def update_texture_pointer(self, context):
if self.pointer:
# handle replacing pointer value
if self.pointer_old\
and context.scene.wow_m2_root_elements.textures.find(self.pointer.name) < 0:
self.pointer_old.wow_m2_texture.enabled = False
self.pointer_old = None
# check if texture is used
if self.pointer.wow_m2_texture.enabled \
and context.scene.wow_m2_root_elements.textures.find(self.pointer.name) >= 0:
self.pointer = None
return
self.pointer.wow_m2_texture.enabled = True
self.pointer.wow_m2_texture.self_pointer = self.pointer
self.pointer_old = self.pointer
self.name = self.pointer.name
# force pass index recalculation
ctx_override = namedtuple('ctx_override', ('texture',))
ctx = ctx_override(self.pointer)
elif self.pointer_old:
# handle deletion
self.pointer_old.wow_m2_texture.enabled = False
self.pointer_old = None
self.name = ""
class M2GeosetPointerPropertyGroup(bpy.types.PropertyGroup):
pointer: bpy.props.PointerProperty(
name='M2 Geoset',
type=bpy.types.Object,
poll=lambda self, obj: is_obj_unused(obj) and obj.type == 'MESH',
# update=lambda self, ctx: update_object_pointer(self, ctx, 'wow_m2_geoset', 'GEOSET')
update=update_geoset_pointer
)
pointer_old: bpy.props.PointerProperty(type=bpy.types.Object)
name: bpy.props.StringProperty()
class M2EventPointerPropertyGroup(bpy.types.PropertyGroup):
pointer: bpy.props.PointerProperty(
name='M2 Event',
type=bpy.types.Object,
poll=lambda self, obj: is_obj_unused(obj) and obj.type == 'MESH', #check if mesh works
update=lambda self, ctx: update_object_pointer(self, ctx, 'wow_m2_event', 'EVENT')
)
pointer_old: bpy.props.PointerProperty(type=bpy.types.Object)
name: bpy.props.StringProperty()
class M2AttachmentPointerPropertyGroup(bpy.types.PropertyGroup):
pointer: bpy.props.PointerProperty(
name='M2 Attachment',
type=bpy.types.Object,
poll=lambda self, obj: is_obj_unused(obj)and obj.type == 'MESH', #check if mesh works
update=lambda self, ctx: update_object_pointer(self, ctx, 'wow_m2_attachment', 'ATTACHMENT')
)
pointer_old: bpy.props.PointerProperty(type=bpy.types.Object)
name: bpy.props.StringProperty()
class M2LightPointerPropertyGroup(bpy.types.PropertyGroup):
pointer: bpy.props.PointerProperty(
name='M2 Light',
type=bpy.types.Object,
poll=lambda self, obj: is_obj_unused(obj) and obj.type == 'LIGHT',
update=lambda self, ctx: update_object_pointer(self, ctx, 'wow_m2_light', 'LIGHT')
)
pointer_old: bpy.props.PointerProperty(type=bpy.types.Object)
name: bpy.props.StringProperty()
class M2MaterialPointerPropertyGroup(bpy.types.PropertyGroup):
pointer: bpy.props.PointerProperty(
name='M2 Material',
type=bpy.types.Material,
poll=lambda self, mat: not mat.wow_m2_material.enabled,
update=update_material_pointer
)
pointer_old: bpy.props.PointerProperty(type=bpy.types.Material)
name: bpy.props.StringProperty()
class M2TexturePointerPropertyGroup(bpy.types.PropertyGroup):
pointer: bpy.props.PointerProperty(
name='M2 Texture',
type=bpy.types.Image,
poll=lambda self, text: not text.wow_m2_texture.enabled,
update=update_texture_pointer
)
pointer_old: bpy.props.PointerProperty(type=bpy.types.Image)
name: bpy.props.StringProperty()
class WoWM2_RootComponents(bpy.types.PropertyGroup):
cur_widget: bpy.props.EnumProperty(
name='M2 Components',
items=m2_widget_items
)
geosets: bpy.props.CollectionProperty(type=M2GeosetPointerPropertyGroup)
cur_geoset: bpy.props.IntProperty(update=lambda self, ctx: update_current_object(self, ctx, 'geosets', 'cur_geoset'))
######################################
events: bpy.props.CollectionProperty(type=M2EventPointerPropertyGroup)
cur_event: bpy.props.IntProperty(update=lambda self, ctx: update_current_object(self, ctx, 'events', 'cur_event'))
attachments: bpy.props.CollectionProperty(type=M2AttachmentPointerPropertyGroup)
cur_attachment: bpy.props.IntProperty(update=lambda self, ctx: update_current_object(self, ctx, 'attachments', 'cur_attachment'))
######################################
lights: bpy.props.CollectionProperty(type=M2LightPointerPropertyGroup)
cur_light: bpy.props.IntProperty(update=lambda self, ctx: update_current_object(self, ctx, 'lights', 'cur_light'))
materials: bpy.props.CollectionProperty(type=M2MaterialPointerPropertyGroup)
cur_material: bpy.props.IntProperty()
textures: bpy.props.CollectionProperty(type=M2TexturePointerPropertyGroup)
cur_texture: bpy.props.IntProperty()
# TODO : CAMERAS, PARTICLES, texture transforms ?
def register():
bpy.types.Scene.wow_m2_root_elements = bpy.props.PointerProperty(type=WoWM2_RootComponents)
def unregister():
del bpy.types.Scene.wow_m2_root_elements
@@ -0,0 +1,74 @@
import bpy
from ..enums import *
class SetDefaultTexture(bpy.types.Operator):
"""Sets the texture to the default value 'textures\\ShaneCube.blp'"""
bl_idname = "wow_m2_texture.set_default_texture"
bl_label = "Set Default Texture"
img_name: bpy.props.StringProperty(options={'HIDDEN'})
def execute(self, context):
edit_image = bpy.data.images[self.img_name]
edit_image.wow_m2_texture.path = "textures\\ShaneCube.blp"
return {'FINISHED'}
class M2_PT_texture_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "image"
bl_label = "M2 Texture"
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(context.edit_image.wow_m2_texture, "flags")
col.separator()
col.prop(context.edit_image.wow_m2_texture, "texture_type")
col.separator()
# only show path setting if texture type is hardcoded
if context.edit_image.wow_m2_texture.texture_type == "0":
col.prop(context.edit_image.wow_m2_texture, "path", text='Path')
if(len(context.edit_image.wow_m2_texture.path) == 0):
op = col.operator(SetDefaultTexture.bl_idname, text="Set Default Texture", icon="CONSOLE")
# todo: not a great method, but it should work reliably since this updates every frame
op.img_name = context.edit_image.name
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.image is not None)
class WowM2TexturePropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty()
flags: bpy.props.EnumProperty(
name="Texture flags",
description="WoW M2 texture flags",
items=TEXTURE_FLAGS,
options={"ENUM_FLAG"},
default={'1', '2'}
)
texture_type: bpy.props.EnumProperty(
name="Texture type",
description="WoW M2 texture type",
items=TEXTURE_TYPES
)
path: bpy.props.StringProperty(
name='Path',
description='Path to .blp file in wow file system.'
)
# self_pointer: bpy.props.PointerProperty(type=bpy.types.Image)
def register():
bpy.types.Image.wow_m2_texture = bpy.props.PointerProperty(type=WowM2TexturePropertyGroup)
def unregister():
del bpy.types.Image.wow_m2_texture
@@ -0,0 +1,44 @@
import bpy
class M2_PT_texture_transform_controller_panel(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "M2 Texture Transform"
def draw_header(self, context):
self.layout.prop(context.object.wow_m2_uv_transform, "enabled", text="")
def draw(self, context):
pass
@classmethod
def poll(cls, context):
return (context.scene is not None
and context.scene.wow_scene.type == 'M2'
and context.object is not None
and context.object.type == 'EMPTY'
and not (context.object.wow_m2_event.enabled
or context.object.wow_m2_attachment.enabled
or context.object.wow_m2_camera.enabled
or context.object.wow_m2_ribbon.enabled
or context.object.wow_m2_particle.enabled)
)
class WowM2TextureTransformControllerPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name='Enabled',
description='Enable this object to be WoW M2 texture transform controller',
default=False
)
def register():
bpy.types.Object.wow_m2_uv_transform = bpy.props.PointerProperty(type=WowM2TextureTransformControllerPropertyGroup)
def unregister():
del bpy.types.Object.wow_m2_uv_transform
@@ -0,0 +1,215 @@
import bpy
from ....ui.preferences import get_project_preferences
from ..enums import *
def update_wow_visibility(self, context):
values = self.m2_visibility
for obj in self.objects:
if 'wow_hide' not in obj:
obj['wow_hide'] = obj.hide_get()
if obj['wow_hide'] != obj.hide_get():
continue
if obj.type == "MESH": # only geoset and collision ?
if obj.wow_m2_geoset:
if obj.wow_m2_geoset.collision_mesh:
obj.hide_set('6' not in values)
if obj.wow_m2_geoset.enabled and obj.wow_m2_geoset.collision_mesh == False:
obj.hide_set('0' not in values)
elif obj.wow_m2_attachment.enabled:
obj.hide_set('1' not in values)
elif obj.wow_m2_event.enabled:
obj.hide_set('2' not in values)
elif obj.wow_m2_particle.enabled:
obj.hide_set('4' not in values)
elif obj.type == "LIGHT" and obj.wow_m2_light.enabled:
obj.hide_set('3' not in values)
elif obj.type == "CAMERA" and obj.wow_m2_camera.enabled:
obj.hide_set('5' not in values)
elif obj.type == "ARMATURE": # and obj.data.edit_bones[0].wow_m2_bone: # wow_m2_bone. check if first armature's bone is m2 bone
obj.hide_set('7' not in values)
else:
print("unknown type")
print(obj.name)
print(obj.type)
obj['wow_hide'] = obj.hide_get()
class M2_PT_tools_object_mode_display(bpy.types.Panel):
bl_label = 'M2 Display'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_context = 'objectmode'
bl_category = 'M2'
def draw(self, context):
layout = self.layout.split()
col = layout.column(align=True)
col_row = col.row()
col_row.column(align=True).prop(context.scene, "m2_visibility")
col_col = col_row.column(align=True)
col_col.operator("scene.wow_m2_select_entity", text='', icon='VIEWZOOM').entity = 'wow_m2_geoset'
col_col.operator("scene.wow_m2_select_entity", text='', icon='VIEWZOOM').entity = 'wow_m2_attachment'
col_col.operator("scene.wow_m2_select_entity", text='', icon='VIEWZOOM').entity = 'wow_m2_event'
col_col.operator("scene.wow_m2_select_entity", text='', icon='VIEWZOOM').entity = 'wow_m2_particle'
col_col.operator("scene.wow_m2_select_entity", text='', icon='VIEWZOOM').entity = 'wow_m2_camera'
col_col.operator("scene.wow_m2_select_entity", text='', icon='VIEWZOOM').entity = 'wow_m2_light'
col_col.operator("scene.wow_m2_select_entity", text='', icon='VIEWZOOM').entity = 'Collision'
col_col.operator("scene.wow_m2_select_entity", text='', icon='VIEWZOOM').entity = 'Skeleton'
@classmethod
def poll(cls, context):
return context.scene is not None and context.scene.wow_scene.type == 'M2'
class M2_PT_tools_panel_object_mode_add_to_scene(bpy.types.Panel):
bl_label = 'Add to scene'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_context = 'objectmode'
bl_category = 'M2'
def draw(self, context):
layout = self.layout.split()
game_data_loaded = hasattr(bpy, "wow_game_data") and bpy.wow_game_data.files
col = layout.column(align=True)
col.separator()
col1_col = col.column(align=True)
col1_row1 = col1_col.row(align=True)
col1_row2 = col1_col.row(align=True)
col1_row1.operator("scene.wow_import_last_m2_from_wmv", text='M2',
icon_value=ui_icons['WOW_STUDIO_DOODADS_ADD'])
if proj_prefs := get_project_preferences():
col1_row1.prop(proj_prefs, 'import_method', text='')
col1_row2.operator("scene.m2_add_attachment", text='Attachment', icon='POSE_HLT')
col1_row2.operator("scene.m2_add_event", text='Event', icon='POSE_HLT')
col1_row3 = col1_col.row(align=True)
col1_row3.operator("scene.wow_m2_texture_import", text='Texture', icon='IMAGE_DATA')
col1_row4 = col1_col.row(align=True)
col.separator()
@classmethod
def poll(cls, context):
return context.scene is not None and context.scene.wow_scene.type == 'M2'
class M2_PT_tools_object_mode_actions(bpy.types.Panel):
bl_label = 'Actions'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_context = 'objectmode'
bl_category = 'M2'
def draw(self, context):
layout = self.layout.split()
col = layout.column(align=True)
col.separator()
box_col = col.column(align=True)
col1_row1 = box_col.row(align=True)
col1_row1.operator("scene.m2_ot_enable_drivers", text='Drivers ON', icon='RADIOBUT_ON')
col1_row1.operator("scene.m2_ot_disable_drivers", text='Drivers OFF', icon='RADIOBUT_OFF')
box_col.operator("scene.wow_creature_editor_toggle", text='Creature Editor', icon_value=ui_icons['WOW_STUDIO_SCALE_ADD'])
if bpy.context.selected_objects:
box_col.operator("scene.m2_fill_textures", text='Fill Paths', icon='SEQ_SPLITVIEW')
if context.object and context.object.type == 'ARMATURE':
box_col.operator("object.m2_bone_renamer", text='Rename', icon='CONSOLE')
@classmethod
def poll(cls, context):
return context.scene is not None and context.scene.wow_scene.type == 'M2'
class M2_MT_mesh_wow_components_add(bpy.types.Menu):
bl_label = "WoW"
bl_options = {'REGISTER'}
def draw(self, context):
layout = self.layout
col = layout.column()
if hasattr(bpy, "wow_game_data") and bpy.wow_game_data.files:
col.operator("scene.wow_import_last_m2_from_wmv", text='M2',
icon_value=ui_icons['WOW_STUDIO_DOODADS_ADD'])
col.operator("scene.m2_add_attachment", text='Attachment', icon='POSE_HLT')
col.operator("scene.m2_add_event", text='Event', icon='POSE_HLT')
# col.operator("scene.wow_add_fog", text='Fog', icon_value=ui_icons['WOW_STUDIO_FOG_ADD'])
# col.operator("scene.wow_add_liquid", text='Liquid', icon_value=ui_icons['WOW_STUDIO_LIQUID_ADD'])
# col.operator("scene.wow_add_scale_reference", text='Scale', icon_value=ui_icons['WOW_STUDIO_SCALE_ADD'])
# col.operator("scene.wow_add_light", text='Light', icon='LIGHT')
#
#if hasattr(bpy, "wow_game_data") and bpy.wow_game_data.files:
# col.operator("scene.wow_wmo_import_doodad_from_wmv", text='M2',
# icon_value=ui_icons['WOW_STUDIO_DOODADS_ADD'])
# col.operator("scene.wow_import_last_wmo_from_wmv", text='WMO', icon_value=ui_icons['WOW_STUDIO_WMO_ADD'])
@classmethod
def poll(cls, context):
return context.scene is not None and context.scene.wow_scene.type == 'M2'
def wow_components_add_menu_item(self, context):
self.layout.menu("M2_MT_mesh_wow_components_add", icon_value=ui_icons['WOW_STUDIO_WOW'])
def render_viewport_toggles_right(self, context):
if hasattr(context.scene, 'wow_scene') \
and hasattr(context.scene.wow_scene, 'type') \
and context.scene.wow_scene.type == 'M2':
layout = self.layout
row = layout.row(align=True)
row.popover( panel="M2_PT_tools_object_mode_display"
, text=''
, icon='HIDE_OFF'
)
def register():
bpy.types.Scene.m2_visibility = bpy.props.EnumProperty(
items=[
('0', "Geosets", "Display geosets", 'FILE_3D', 0x1),
('1', "Attachments", "Display attachments", 'POSE_HLT', 0x2),
('2', "Events", "Display events", 'PLUGIN', 0x4),
('3', "Lights", "Display lights", 'LIGHT', 0x8),
('4', "Particles emitters", "Display particle emitters", 'MOD_PARTICLES', 0x10),
('5', "Cameras", "Display cameras", 'CAMERA_DATA', 0x20),
('6', "Collision", "Display collision", 'CON_SIZELIMIT', 0x40),
('7', "Skeleton", "Display bones", 'BONE_DATA', 0x80)],
options={'ENUM_FLAG'},
default={'0', '1', '2', '3', '4', '5', '6', '7'},
update=update_wow_visibility
)
bpy.types.VIEW3D_MT_add.prepend(wow_components_add_menu_item)
bpy.types.VIEW3D_HT_header.append(render_viewport_toggles_right)
def unregister():
del bpy.types.Scene.m2_visibility
bpy.types.VIEW3D_MT_add.remove(wow_components_add_menu_item)
bpy.types.VIEW3D_MT_add.remove(render_viewport_toggles_right)
@@ -0,0 +1,153 @@
import bpy
from ..handlers import DepsgraphLock
_obj_props = ['wow_m2_geoset',
'wow_m2_attachment',
'wow_m2_event',
'wow_m2_light' # bug with light
]
def update_current_object(self, context, col_name, cur_item_name):
if bpy.context.view_layer.objects.active and bpy.context.view_layer.objects.active.mode != 'OBJECT':
return
col = getattr(self, col_name)
cur_idx = getattr(self, cur_item_name)
if len(col) <= cur_idx or cur_idx < 0:
return
slot = col[cur_idx]
if bpy.context.view_layer.objects.active == slot.pointer:
return
if slot.pointer and not slot.pointer.hide_get():
with DepsgraphLock():
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.objects.active = slot.pointer
slot.pointer.select_set(True)
def is_obj_unused(obj):
for prop in _obj_props:
if prop == 'wow_m2_light':
if obj.type == 'LIGHT' and getattr(obj.data, prop).enabled:
return False
else:
if getattr(obj, prop).enabled:
return False
# if obj.wow_m2_geoset.collision_mesh:
# return False
return True
class M2_UL_root_elements_template_list(bpy.types.UIList):
icon = 'OBJECT_DATA'
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
if self.layout_type in {'DEFAULT', 'COMPACT'}:
# handle material icons
if self.icon == 'MATERIAL_DYNAMIC':
texture = item.pointer.wow_m2_material.texture_1 if item.pointer else None
self.icon = layout.icon(texture) if texture else 'MATERIAL'
elif self.icon == 'TEXTURE_DYNAMIC':
texture = item.pointer if item.pointer else None
self.icon = layout.icon(texture) if texture else 'MATERIAL'
row = layout.row()
col = row.column()
col.scale_x = 0.5
if isinstance(self.icon, int):
col.label(text="#{} ".format(index), icon_value=self.icon)
elif isinstance(self.icon, str):
col.label(text="#{} ".format(index), icon=self.icon)
col = row.column()
s_row = col.row(align=True)
s_row.prop(item, 'pointer', emboss=True, text='')
elif self.layout_type in {'GRID'}:
pass
def filter_items(self, context, data, propname):
col = getattr(data, propname)
filter_name = self.filter_name.lower()
flt_flags = [self.bitflag_filter_item
if any(filter_name in filter_set for filter_set in (str(i), (item.pointer.name if item.pointer else 'Empty slot').lower()))
else 0 for i, item in enumerate(col, 1)
]
if self.use_filter_sort_alpha:
flt_neworder = [x[1] for x in sorted(
zip(
[x[0] for x in sorted(enumerate(col),
key=lambda x: x[1].name.split()[1] + x[1].name.split()[2])], range(len(col))
)
)
]
else:
flt_neworder = []
return flt_flags, flt_neworder
class M2_OT_object_list_change(bpy.types.Operator):
bl_idname = 'object.wow_m2_object_list_change'
bl_label = 'Add / Remove'
bl_description = 'Add / Remove'
bl_options = {'REGISTER','INTERNAL','UNDO'}
obj_name: bpy.props.StringProperty(options={'HIDDEN'})
col_name: bpy.props.StringProperty(options={'HIDDEN'})
idx_name: bpy.props.StringProperty(options={'HIDDEN'})
action: bpy.props.StringProperty(default='ADD', options={'HIDDEN'})
add_action: bpy.props.EnumProperty(
items=[('EMPTY', 'Empty', ''),
('NEW', 'New', '')],
default='EMPTY',
options={'HIDDEN'}
)
def execute(self, context):
obj = getattr(bpy.context.object,self.obj_name)
col = getattr(obj,self.col_name)
if self.action == 'ADD':
slot = col.add()
setattr(obj,self.idx_name,len(col)-1)
elif self.action == 'REMOVE':
idx = getattr(obj,self.idx_name)
col_len = len(col)
if idx >= col_len or idx < 0:
idx = col_len-1
col.remove(getattr(obj,self.idx_name))
else:
raise ValueError(f'Invalid object list action {self.action}')
return {'FINISHED'}
def draw_object_list(obj,col,label,template,obj_name,col_name,sel_name):
col.label(text=label)
row = col.row()
sub_col1 = row.column()
sub_col1.template_list(template,'',obj,col_name,obj,sel_name)
sub_col_parent = row.column()
sub_col2 = sub_col_parent.column(align=True)
op1 = sub_col2.operator('object.wow_m2_object_list_change', text='', icon='ADD')
op1.action, op1.col_name, op1.idx_name, op1.obj_name = 'ADD', col_name, sel_name, obj_name
op2 = sub_col2.operator('object.wow_m2_object_list_change', text='', icon='REMOVE')
op2.action, op2.col_name, op2.idx_name, op2.obj_name = 'REMOVE', col_name, sel_name, obj_name
@@ -0,0 +1,181 @@
import bpy
# ---------------------------
# Animation utilities
# ---------------------------
def can_apply_scale(fcurves, keyframe_count):
for i in range(keyframe_count):
x = fcurves[0].keyframe_points[i].co[1] if 0 in fcurves else 1
y = fcurves[1].keyframe_points[i].co[1] if 1 in fcurves else 1
z = fcurves[2].keyframe_points[i].co[1] if 2 in fcurves else 1
if abs(x - y) > 0.0001 or abs(x - z) > 0.0001:
return False
return True
def make_fcurve_compound(fcurves, accept=lambda path: True):
compound = {}
for fcurve in fcurves:
if not accept(fcurve.data_path):
continue
if fcurve.data_path not in compound:
compound[fcurve.data_path] = {}
compound[fcurve.data_path][fcurve.array_index] = fcurve
return compound
def get_bone_groups(obj, vertex, bone_names):
groups = [el for el in vertex.groups if obj.vertex_groups[el.group].name in bone_names]
groups.sort(key=lambda x: -x.weight)
return groups
def _find_final_alias(self, n_global_sequences, alias_next):
for anim_index in self.animations:
anim = bpy.context.scene.wow_m2_animations[alias_next + n_global_sequences]
if '64' in anim.flags:
alias_next = anim.alias_next
else:
return alias_next + n_global_sequences
# ---------------------------
# Collection Management
# ---------------------------
def get_or_create_collection(name, parent=None, color_tag="NONE"):
"""Get or create a collection by name, optionally under a parent."""
if name in bpy.data.collections:
col = bpy.data.collections[name]
else:
col = bpy.data.collections.new(name)
if parent:
parent.children.link(col)
else:
bpy.context.scene.collection.children.link(col)
col.color_tag = color_tag
return col
def find_m2_root_collection():
"""Locate the top-level M2 collection in the scene."""
return next(
(c for c in bpy.context.scene.collection.children if c.get("wow_m2_collection")),
None
)
def _link_to_single_collection(obj, collection):
"""Ensure obj is linked only to the given collection."""
for col in obj.users_collection:
col.objects.unlink(obj)
if obj.name in bpy.context.scene.collection.objects:
bpy.context.scene.collection.objects.unlink(obj)
collection.objects.link(obj)
def get_or_create_m2_collection(target):
"""
Ensure a top-level M2 collection exists and required sub-collections are present.
target = name (str) or Collection
Returns: (main_collection, dict{sub collections})
"""
M2_SUBS = {
"Armature": "COLOR_02",
"Geosets": "COLOR_02",
"Color_Transparency": "COLOR_02",
"Texture_Transforms": "COLOR_02",
"Events": "COLOR_02",
"Attachments": "COLOR_02",
"Cameras": "COLOR_02",
"Collision": "COLOR_02",
"Particles": "COLOR_02",
"Ribbons": "COLOR_02",
"Lights": "COLOR_02",
}
# Resolve target
if isinstance(target, str):
name = target
main_col = bpy.data.collections.get(name) or get_or_create_collection(name)
elif isinstance(target, bpy.types.Collection):
main_col = target
name = main_col.name
else:
raise TypeError("target must be a string or bpy.types.Collection")
# Must be top-level
if main_col.name not in bpy.context.scene.collection.children:
raise ValueError(f"Collection '{name}' exists but is not top-level")
# Mark collection as M2 root
if "wow_m2_collection" not in main_col:
main_col["wow_m2_collection"] = True
# Ensure global flags pointer exists
if hasattr(main_col, "wow_m2_globalflags"):
main_col.wow_m2_globalflags.enabled = True
# Create sub-collections
subs = {}
for sub, col_tag in M2_SUBS.items():
subs[sub.lower()] = get_or_create_collection(sub, main_col, col_tag)
# Build ColorTransparencyController
ensure_color_transparency_controller(subs["color_transparency"])
return main_col, subs
# ---------------------------
# Color/Transparency Controller
# ---------------------------
def find_color_transparency_controller():
"""Return the M2 ColorTransparencyController object, or None."""
m2_root = find_m2_root_collection()
if not m2_root:
return None
def walk(col):
for o in col.objects:
yield o
for sub in col.children:
yield from walk(sub)
for obj in walk(m2_root):
if obj.get("is_color_transparency_controller") or obj.name == "ColorTransparencyController":
return obj
return None
def ensure_color_transparency_controller(color_trans_collection):
"""Ensure the ColorTransparencyController exists in this M2 set."""
from .operations import m2_action_logger as log
m2_root = None
for coll in bpy.context.scene.collection.children:
if any(child is color_trans_collection for child in coll.children):
m2_root = coll
break
if not m2_root:
raise RuntimeError("Could not locate parent M2 root collection")
controller = find_color_transparency_controller()
if not controller:
bpy.ops.object.empty_add(type="CUBE", location=(0, 0, 0))
controller = bpy.context.view_layer.objects.active
controller.name = "ColorTransparencyController"
controller.scale = (0.03, 0.03, 0.03)
controller["is_color_transparency_controller"] = True
if hasattr(controller, "wow_m2_color_transparency"):
controller.wow_m2_color_transparency.enabled = True
_link_to_single_collection(controller, color_trans_collection)
log.debug(f"Created ColorTransparencyController in '{m2_root.name}'")
else:
log.debug(f"Reusing ColorTransparencyController in '{m2_root.name}'")
if color_trans_collection not in controller.users_collection:
_link_to_single_collection(controller, color_trans_collection)
controller.animation_data_create()
controller.animation_data.action_blend_type = "ADD"
props = getattr(controller, "wow_m2_color_transparency", None)
return controller, props