новая структура проекта
This commit is contained in:
+42
@@ -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))
|
||||
+238
@@ -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])
|
||||
BIN
Binary file not shown.
+60
@@ -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
+185
@@ -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()
|
||||
+157
@@ -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)
|
||||
+255
@@ -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)
|
||||
+93
@@ -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'}
|
||||
+85
@@ -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'}
|
||||
+57
@@ -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)
|
||||
+78
@@ -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'}
|
||||
+83
@@ -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'}
|
||||
+53
@@ -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'}
|
||||
+174
@@ -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'}
|
||||
+188
@@ -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'}
|
||||
+1261
File diff suppressed because it is too large
Load Diff
+55
@@ -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'
|
||||
+62
@@ -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
|
||||
|
||||
+537
@@ -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
|
||||
+181
@@ -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
|
||||
+496
@@ -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
|
||||
+82
@@ -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
|
||||
+57
@@ -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
|
||||
|
||||
+96
@@ -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
|
||||
+108
@@ -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
|
||||
+68
@@ -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
|
||||
+752
@@ -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
|
||||
+470
@@ -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
|
||||
|
||||
+172
@@ -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
|
||||
+523
@@ -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
|
||||
|
||||
+74
@@ -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
|
||||
+44
@@ -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
|
||||
+215
@@ -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)
|
||||
+153
@@ -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
|
||||
Reference in New Issue
Block a user