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

This commit is contained in:
2026-04-20 21:04:25 +04:00
parent 1fe2a72ef1
commit 1a56b22e38
1932 changed files with 1886 additions and 22779 deletions
@@ -0,0 +1,211 @@
from ...utils.collections import SpecialCollection, obj_swap_collections, collection_swap_parent_collection, get_or_create_collection, get_current_wow_model_collection
from ...utils.bl_id_types_utils import match_id_name
from ...ui.message_stack import MessageStack
from .enums import SpecialCollections
from .custom_objects import WoWWMOGroup, WoWWMOLight, WoWWMOFog\
, WoWWMOLiquid, WoWWMODoodad, WoWWMOCollision, WoWWMOPortal
from typing import Iterable
import bpy
def get_wmo_collection(scene: bpy.types.Scene,
collection_type: SpecialCollections) -> bpy.types.Collection | None:
wow_model_collection = get_current_wow_model_collection(scene, 'wow_wmo')
if wow_model_collection:
return get_or_create_collection(wow_model_collection, collection_type.name)
else:
return None
def iter_wmo_groups(scene: bpy.types.Scene) -> bpy.types.Object:
""" Iterate and return each Object in Indoor and Outdor group collections
How to use : 'for each in iter_wmo_groups(scene):
print(each)'
"""
for each in get_wmo_collection(scene, SpecialCollections.Outdoor).objects:
yield each
for each in get_wmo_collection(scene, SpecialCollections.Indoor).objects:
yield each
def get_wmo_groups_list(scene: bpy.types.Scene) -> list[bpy.types.Object]:
groups_list = []
for each in iter_wmo_groups(scene):
groups_list.append(each)
return groups_list
class _WMOCollection:
""" Base class for all WMO collections. """
__wbs_root_collection_id_prop_name__ = 'wow_wmo'
class _GroupCollection(_WMOCollection):
""" Base class for WMO group collections. """
__wbs_bl_object_types__ = {'MESH'}
__wbs_custom_object_types__ = {WoWWMOGroup}
class OutdoorGroupCollection(_GroupCollection, SpecialCollection):
""" Collection of objects marked as outdoor group. Accepts any MESH objects
except for those matching other types.
"""
__wbs_collection_name__ = SpecialCollections.Outdoor.name
class IndoorGroupCollection(_GroupCollection, SpecialCollection):
""" Collection of objects marked as indoor group. Accepts any MESH objects
except for those matching other types.
"""
__wbs_collection_name__ = SpecialCollections.Indoor.name
class LightCollection(_WMOCollection, SpecialCollection):
""" Collection of objects marked as lights. Only accepts lights created through an operator."""
__wbs_collection_name__ = SpecialCollections.Lights.name
__wbs_bl_object_types__ = {'LIGHT'}
__wbs_custom_object_types__ = {WoWWMOLight}
class CollisionCollection(_WMOCollection, SpecialCollection):
""" Collection of objects marked as collision. Accepts any MESH objects
except for those matching other types.
"""
__wbs_collection_name__ = SpecialCollections.Collision.name
__wbs_bl_object_types__ = {'MESH'}
__wbs_custom_object_types__ = {WoWWMOCollision}
class FogCollection(_WMOCollection, SpecialCollection):
""" Collection of objects marked as fogs. Only accepts fogs created through an operator. """
__wbs_collection_name__ = SpecialCollections.Fogs.name
__wbs_bl_object_types__ = {'MESH'}
__wbs_custom_object_types__ = {WoWWMOFog}
class LiquidCollection(_WMOCollection, SpecialCollection):
""" Collection of objects marked as liquids. Only accepts liquids created through an operator. """
__wbs_collection_name__ = SpecialCollections.Liquids.name
__wbs_bl_object_types__ = {'MESH'}
__wbs_custom_object_types__ = {WoWWMOLiquid}
class PortalCollection(_WMOCollection, SpecialCollection):
""" Collection of objects marked as portals. Accepts any MESH objects
except for those matching other types.
"""
__wbs_collection_name__ = SpecialCollections.Portals.name
__wbs_bl_object_types__ = {'MESH'}
__wbs_custom_object_types__ = {WoWWMOPortal}
class DoodadSetsCollection(_WMOCollection, SpecialCollection):
""" Collection of collections representing doodad sets. Does not accept any objects.
Child collections accept objects marked as doodads (created by operators).
"""
__wbs_collection_name__ = SpecialCollections.Doodads.name
__wbs_bl_object_types__ = {'MESH'}
__wbs_custom_object_types__ = {WoWWMODoodad}
@classmethod
def is_doodad_set_collection(cls
, scene: bpy.types.Scene
, col: bpy.types.Collection) -> bool:
"""
Identify if a collection is a doodad set collection (child collection of special collection Doodads)
:param scene: Current scene.
:param col: Collection to test.
:return: True if is doodad set collection, else False.
"""
for scene_col_child in scene.collection.children:
# check if we are inside the WMO collection
if getattr(scene_col_child, cls.__wbs_root_collection_id_prop_name__).enabled:
for wmo_col_child in scene_col_child.children:
if match_id_name(wmo_col_child.name, cls.__wbs_collection_name__) \
and col.name in wmo_col_child.children:
return True
return False
@classmethod
def verify_doodad_sets_collection_integrity(cls
, scene: bpy.types.Scene
, root_col: bpy.types.Collection):
if root_col is None:
return False
for child_col in root_col.children:
if match_id_name(child_col.name, DoodadSetsCollection.__wbs_collection_name__):
default_global_coll = None
for col in child_col.children:
if col.name.startswith("Set_$DefaultGlobal"):
default_global_coll = col
break
if not default_global_coll:
default_global_coll = bpy.data.collections.new("Set_$DefaultGlobal")
child_col.children.link(default_global_coll)
default_global_coll.color_tag = 'COLOR_04'
@classmethod
def handle_collection_if_matched(cls
, scene: bpy.types.Scene
, update: bpy.types.DepsgraphUpdate
, special_collection_ts: Iterable['SpecialCollection']) -> bool:
collection: bpy.types.Collection = update.id.original
# test if collection is within a root collection
root_col = cls._get_root_collection(scene, collection)
if root_col is None:
return False
cls.verify_root_collection_integrity(root_col, special_collection_ts)
cls.verify_doodad_sets_collection_integrity(scene, root_col)
# doodad sets collection of collections
if match_id_name(collection.name, cls.__wbs_collection_name__):
# make sure it has no object linked to it directly
if len(collection.objects):
for obj in collection.objects:
obj_swap_collections(obj, collection, root_col)
MessageStack().push_message(msg=f'Collection "{collection.name}" can only contain child collections.')
# doodads set individual collection
elif cls.is_doodad_set_collection(scene, collection):
# make sure no collection is linked to it directly
if len(collection.children):
for child_col in collection.children:
collection_swap_parent_collection(child_col, collection, root_col)
child_col.color_tag = 'COLOR_04'
MessageStack().push_message(msg=f'Collection "{collection.name}" cannot contain child collections.')
for obj in collection.objects:
# handle custom object types (doodads)
if cls.__wbs_custom_object_types__ is not None \
and not any(c_obj.match(obj) for c_obj in cls.__wbs_custom_object_types__):
obj_swap_collections(obj, collection, root_col)
MessageStack().push_message(msg=f'Object "{obj.name}" removed from special collection '
f'"{cls.__wbs_collection_name__}" due to not matching '
f'with any custom object type.', icon='ERROR')
else:
return False
return True
WMO_SPECIAL_COLLECTION_TYPES = (
OutdoorGroupCollection,
IndoorGroupCollection,
FogCollection,
PortalCollection,
LiquidCollection,
LightCollection,
CollisionCollection,
DoodadSetsCollection
)
@@ -0,0 +1,277 @@
from ...utils.custom_object import CustomObject
from ...utils.bl_id_types_utils import match_id_name
from ..bl_render import BlenderWMOObjectRenderFlags
from .enums import SpecialColorLayers
from functools import partial
import bpy
_OBJECT_MODE_DESTRUCTIVE_OPS = {
'OBJECT_OT_transform_apply',
'OBJECT_OT_transforms_to_deltas',
'OBJECT_OT_origin_set',
'TRANSFORM_OT_mirror',
'OBJECT_OT_visual_transform_apply'
}
class WoWWMOGroup(CustomObject):
__wbs_bl_object_type__ = 'MESH'
__wbs_prop_group_id__ = 'wow_wmo_group'
_bl_render_renderflag_map = {
SpecialColorLayers.Lightmap.name: BlenderWMOObjectRenderFlags.HasLightmap,
SpecialColorLayers.BatchmapInt.name: BlenderWMOObjectRenderFlags.HasBatchB,
SpecialColorLayers.BatchmapTrans.name: BlenderWMOObjectRenderFlags.HasBatchA,
SpecialColorLayers.Blendmap.name: BlenderWMOObjectRenderFlags.HasBlendmap
}
@classmethod
def match(cls, obj: bpy.types.Object) -> bool:
return super().match(obj) \
and any(match_id_name(col.name, 'Indoor')
or match_id_name(col.name, 'Outdoor') for col in obj.users_collection)
@classmethod
def is_indoor(cls, obj: bpy.types.Object) -> bool:
if WoWWMOGroup.match(obj):
for col in obj.users_collection:
if match_id_name(col.name, 'Indoor'):
return True
return False
else:
return False
@classmethod
def is_outdoor(cls, obj: bpy.types.Object) -> bool:
if WoWWMOGroup.match(obj):
for col in obj.users_collection:
if match_id_name(col.name, 'Outdoor'):
return True
return False
else:
return False
@classmethod
def handle_object_if_matched(cls
, update: bpy.types.DepsgraphUpdate) -> bool:
obj: bpy.types.Object = update.id.original
if not cls.match(obj):
return False
cls.on_each_update(update)
return True
@classmethod
def on_each_update(cls, update: bpy.types.DepsgraphUpdate) -> bool:
obj: bpy.types.Object = update.id.original
mesh: bpy.types.Mesh = obj.data
# change shader behavior depending on the presence of specially named color layers
for col_name, flag in cls._bl_render_renderflag_map.items():
col = mesh.color_attributes.get(col_name)
if col:
obj.pass_index |= flag
else:
obj.pass_index &= ~flag
# update shader based on current place type
if cls.is_outdoor(obj):
obj.pass_index |= BlenderWMOObjectRenderFlags.IsOutdoor
obj.pass_index &= ~BlenderWMOObjectRenderFlags.IsIndoor
else:
obj.pass_index &= ~BlenderWMOObjectRenderFlags.IsOutdoor
obj.pass_index |= BlenderWMOObjectRenderFlags.IsIndoor
return True
class WoWWMOFog(CustomObject):
__wbs_bl_object_type__ = 'MESH'
__wbs_prop_group_id__ = 'wow_wmo_fog'
__wbs_allowed_modes__ = set()
__wbs_allow_scale__ = True
__wbs_allow_non_uniform_scale__ = False
__wbs_allow_rotation__ = False
__wbs_allow_modifiers__ = False
__wbs_allow_constraints__ = False
__wbs_allow_material_properties__ = False
__wbs_allow_mesh_properties__ = False
__wbs_allow_particles__ = False
__wbs_allow_physics__ = False
__wbs_banned_ops__ = _OBJECT_MODE_DESTRUCTIVE_OPS
class WoWWMODoodad(CustomObject):
__wbs_bl_object_type__ = 'MESH'
__wbs_prop_group_id__ = 'wow_wmo_doodad'
__wbs_allowed_modes__ = set()
__wbs_allow_scale__ = True
__wbs_allow_non_uniform_scale__ = False
__wbs_allow_rotation__ = True
__wbs_allow_modifiers__ = False
__wbs_allow_constraints__ = False
__wbs_allow_material_properties__ = False
__wbs_allow_mesh_properties__ = False
__wbs_allow_particles__ = False
__wbs_allow_physics__ = False
__wbs_banned_ops__ = _OBJECT_MODE_DESTRUCTIVE_OPS
@classmethod
def on_each_update(cls, update: bpy.types.DepsgraphUpdate) -> bool:
obj: bpy.types.Object = update.id.original
# handle object copies
if obj.active_material:
if obj.active_material.users > 1:
for i, mat in enumerate(obj.data.materials):
mat = mat.copy()
obj.data.materials[i] = mat
return True
class WoWWMOLight(CustomObject):
__wbs_bl_object_type__ = 'LIGHT'
__wbs_prop_group_id__ = 'wow_wmo_light'
__wbs_allowed_modes__ = set()
__wbs_allow_scale__ = False
__wbs_allow_non_uniform_scale__ = False
__wbs_allow_rotation__ = False
__wbs_allow_modifiers__ = False
__wbs_allow_constraints__ = False
__wbs_allow_material_properties__ = False
__wbs_allow_particles__ = False
__wbs_allow_physics__ = False
__wbs_banned_ops__ = set()
class WoWWMOLiquid(CustomObject):
__wbs_bl_object_type__ = 'MESH'
__wbs_prop_group_id__ = 'wow_wmo_liquid'
__wbs_allowed_modes__ = {'EDIT', 'SCULPT', 'VERTEX_PAINT'}
__wbs_allow_scale__ = False
__wbs_allow_non_uniform_scale__ = False
__wbs_allow_rotation__ = False
__wbs_allow_modifiers__ = False
__wbs_allow_constraints__ = False
__wbs_allow_material_properties__ = False
__wbs_allow_particles__ = False
__wbs_allow_physics__ = False
__wbs_banned_ops__ = {
'TRANSFORM_OT_mirror',
'TRANSFORM_OT_mirror',
'MESH_OT_delete',
'MESH_OT_duplicate_move',
'MESH_OT_extrude_region',
'MESH_OT_extrude_verts_indiv',
'MESH_OT_split',
'MESH_OT_symmetrize',
'MESH_OT_sort_elements',
'MESH_OT_delete_loose',
'MESH_OT_decimate',
'MESH_OT_dissolve_degenerate',
'MESH_OT_dissolve_limited',
'MESH_OT_face_make_planar',
'MESH_OT_face_make_planar',
'MESH_OT_vert_connect_nonplanar',
'MESH_OT_vert_connect_concave',
'MESH_OT_bevel',
'MESH_OT_merge'
}
@staticmethod
def _liquid_edit_mode_timer(context: bpy.types.Context):
bpy.ops.wow.liquid_edit_mode(context, 'INVOKE_DEFAULT')
@staticmethod
def _edit_mode(_: bpy.types.DepsgraphUpdate):
win = bpy.context.window
# avoid focusing settings window if left open
if win.screen.name == 'temp':
for win_ in bpy.context.window_manager.windows:
if win_.screen.name != 'temp':
win = win_
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'][0]
space = [space for space in areas3d[0].regions if space.type == 'VIEW_3D']
override = {'window': win,
'screen': scr,
'area': areas3d[0],
'region': region,
'scene': bpy.context.scene,
'workspace': bpy.context.workspace,
'space_data': space,
'region_data': region
}
# we need a timer here to prevent operator recognizing tab event as exit
bpy.app.timers.register(partial(WoWWMOLiquid._liquid_edit_mode_timer, override), first_interval=0.1)
@staticmethod
def _sculpt_mode(_: bpy.types.DepsgraphUpdate):
for brush in bpy.data.brushes:
brush.sculpt_plane = 'Z'
__wbs_on_mode_handlers__ = {
'EDIT': _edit_mode,
'SCULPT': _sculpt_mode
}
class WoWWMOPortal(CustomObject):
__wbs_bl_object_type__ = 'MESH'
__wbs_prop_group_id__ = 'wow_wmo_portal'
__wbs_allowed_modes__ = {'EDIT'}
__wbs_allow_modifiers__ = False
__wbs_allow_mesh_properties__ = False
__wbs_allow_particles__ = False
__wbs_allow_physics__ = False
@classmethod
def match(cls, obj: bpy.types.Object) -> bool:
return super().match(obj) \
and any(match_id_name(col.name, 'Portals') for col in obj.users_collection)
class WoWWMOCollision(CustomObject):
__wbs_bl_object_type__ = 'MESH'
__wbs_prop_group_id__ = ''
__wbs_allow_modifiers__ = True
__wbs_allow_constraints__ = False
__wbs_allow_material_properties__ = False
__wbs_allow_particles__ = False
__wbs_allow_physics__ = False
@classmethod
def match(cls, obj: bpy.types.Object) -> bool:
return super().match(obj) \
and any(match_id_name(col.name, 'Collision') for col in obj.users_collection)
@classmethod
def handle_object_if_matched(cls
, update: bpy.types.DepsgraphUpdate) -> bool:
obj: bpy.types.Object = update.id.original
return cls.match(obj)
WMO_CUSTOM_OBJECT_TYPES = (
WoWWMOGroup,
WoWWMOFog,
WoWWMOPortal,
WoWWMOLight,
WoWWMOLiquid,
WoWWMOCollision,
WoWWMODoodad
)
""" Tuple of all custom objects defined in this file. """
@@ -0,0 +1,155 @@
from ... import ui_icons
from enum import Enum, auto
__reload_order_index__ = -1
shader_enum = [
('0', "Diffuse", ""),
('1', "Specular", ""),
('2', "Metal", ""),
('3', "Env", ""),
('4', "Opaque", ""),
('5', "EnvMetal", ""),
('6', "TwoLayerDiffuse", ""),
('7', "TwoLayerEnvMetal", ""),
('8', "TwoLayerTerrain", ""),
('9', "DiffuseEmissive", ""),
('10', "waterWindow", ""),
('11', "MaskedEnvMetal", ""),
('12', "EnvMetalEmissive", ""),
('13', "TwoLayerDiffuseOpaque", ""),
('14', "submarineWindow", ""),
('15', "TwoLayerDiffuseEmissive", ""),
('16', "DiffuseTerrain", ""),
('17', "AdditiveMaskedEnvMetal", ""),
('18', "TwoLayerDiffuseMod2x", ""),
('19', "TwoLayerDiffuseMod2xNA", ""),
('20', "TwoLayerDiffuseAlpha", ""),
('21', "MapObjLod", ""),
('22', "MapObjParallax", "")
]
terrain_type_enum = [ # can be loaded from DBC. TODO: legion terrain types
('0', "Dirt", ""), ('1', "Metallic", ""), ('2', "Stone", ""),
('3', "Snow", ""), ('4', "Wood", ""), ('5', "Grass", ""),
('6', "Leaves", ""), ('7', "Sand", ""), ('8', "Soggy", ""),
('9', "Dusty Grass", ""), ('10', "None", ""), ('11', "Water", ""),
('12', "Unknown", ""),('13', "Unknown", ""),
]
blending_enum = [
('0', "Blend_Opaque", ""), ('1', "Blend_AlphaKey", ""), ('2', "Blend_Alpha", ""),
('3', "Blend_Add", ""), ('4', "Blend_Mod", ""), ('5', "Blend_Mod2x", ""),
('6', "Blend_ModAdd", ""), ('7', "Blend_InvSrcAlphaAdd", ""), ('8', "Blend_InvSrcAlphaOpaque", ""),
('9', "Blend_SrcAlphaOpaque", ""), ('10', "Blend_NoAlphaAdd", ""), ('11', "Blend_ConstantAlpha", "")
]
material_flag_enum = [
("1", "Unlit", "Disable lighting", 'SHADING_SOLID', 0x1),
("2", "Unfogged", "Disable fog", 'MOD_FLUID', 0x2),
("4", "Two-sided", "Render from both sides", 'MOD_UVPROJECT', 0x4),
("8", "Exterior light", "Ignore local WMO lighting, use world lighting instead", 'LIGHT_SUN', 0x8),
("16", "Night Glow", "Used for windows to glow at nighttime", ui_icons['MAT_NIGHT_GLOW'], 0x10),
("32", "Window", "Unknown, used for windows", ui_icons['MAT_WINDOW'], 0x20),
("64", "Clamp_S", "Force texture to use clamp _s adressing", 'DISCLOSURE_TRI_DOWN', 0x40),
("128", "Clamp_T", "Force texture to use clamp _t adressing", 'TRIA_DOWN', 0x80)
]
group_flag_enum = [
('0', "Vertex color", "Check if you need vertex color in this group", 'SHADING_RENDERED', 0x1),
('1', "No local lighting", "Use world-defined lighting in a group", 'LIGHT', 0x2),
('2', "Always draw", "Always draw the model. Disable portal-based geometry culling", 'SHAPEKEY_DATA', 0x4),
('3', "Mounts allowed", "Allow mounts in this indoor group", 'MESH_MONKEY', 0x8),
('4', "Use Skybox", "Display WMO skybox in this indoor group", 'SURFACE_NSPHERE', 0x10),
('5', "Show exterior sky", "Show exterior sky in interior WMO group", 'MAT_SPHERE_SKY', 0x20)
]
place_type_enum = [
('8', "Outdoor", "", 'OBJECT_DATA', 0),
('8192', "Indoor", "", 'MOD_SUBSURF', 1)
]
liquid_type_enum = [
('0', "No liquid", ""), ('1', "Water", ""), ('2', "Ocean", ""),
('3', "Magma", ""), ('4', "Slime", ""), ('5', "Slow Water", ""),
('6', "Slow Ocean", ""), ('7', "Slow Magma", ""), ('8', "Slow Slime", ""),
('9', "Fast Water", ""), ('10', "Fast Ocean", ""), ('11', "Fast Magma", ""),
('12', "Fast Slime", ""), ('13', "WMO Water", ""), ('14', "WMO Ocean", ""),
('15', "Green Lava", ""), ('17', "WMO Water - Interior", ""), ('19', "WMO Magma", ""),
('20', "WMO Slime", ""), ('21', "Naxxramas - Slime", ""), ('41', "Coilfang Raid - Water", ""),
('61', "Hyjal Past - Water", ""), ('81', "Lake Wintergrasp - Water", ""), ('100', "Basic Procedural Water", ""),
('121', "CoA Black - Magma", ""), ('141', "Chamber Magma", ""), ('181', "Orange Slime", "")
]
portal_dir_alg_enum = [
("0", "Auto", "", 'MONKEY', 0),
("1", "Positive", "", 'ADD', 1),
("2", "Negative", "", 'REMOVE', 2)
]
portal_detail_enum = [
("0", "None", ""),
("1", "First", ""),
("2", "Second", "")
]
root_flags_enum = [
("0", "No Auto Attenuation", "Do not attenuate light on vertices based on distance from portal", 'NODE_TEXTURE', 0x1),
("1", "Do Not Use Lightmap", "", 'LIGHT_SUN', 0x2),
("2", "Unified rendering", "Use ambient lighting inside indoor groups", 'OUTLINER_OB_LIGHT', 0x4),
# ("3", "Use real Liquid Type", "Use real liquid type ID from DBCs instead of local one.\nDisabling this might result in filling the WMO with water.", 'MOD_OCEAN', 0x8)
]
light_type_enum = [
('0', "Omni", ""), ('1', "Spot", ""),
('2', "Direct", ""), ('3', "Ambient", "")
]
class SpecialCollections(Enum):
"""
Represents names of collections that have special meaning within the addon.
"""
Outdoor = auto()
""" Collection used to store outdoor WMO groups. """
Indoor = auto()
""" Collection used to store indor WMO groups. """
Lights = auto()
""" Collection used to store lights. """
Fogs = auto()
""" Collection used to store fogs. """
Liquids = auto()
""" Collection used to store liquids such as water or lava. """
Portals = auto()
""" Collection used to store portal planes. """
Doodads = auto()
""" Collection used to store doodad sets (collections of doodads). """
Collision = auto()
""" Collection used to store WoW coliision. """
class SpecialColorLayers(Enum):
"""
Represents vertex color layer names that have special meaning within the addon.
"""
Lightmap = auto()
""" Color representing the lightmap (blending of exterior and interior lighting). """
BatchmapInt = auto()
""" Color layer representing markup of batch types for interior batches. """
BatchmapTrans = auto()
""" Color layer representing markup of batch types for transitional batches. (priority over BatchmapInt). """
Blendmap = auto()
""" Color layer representing blending mask for materials that use multiple textures. """
@@ -0,0 +1,86 @@
from ...ui.locks import DepsgraphLock
from ...ui.message_stack import MessageStack
from ...utils.collections import SpecialCollection
from ..ui.custom_objects import WMO_CUSTOM_OBJECT_TYPES
from ..ui.collections import WMO_SPECIAL_COLLECTION_TYPES
from ...ui.enums import WoWSceneTypes
from .collections import DoodadSetsCollection
from bpy.app.handlers import persistent
import bpy
def handle_material_update(update: bpy.types.DepsgraphUpdate):
"""
Handles update to materials.
:param update: Current update.
"""
... # TODO
def handle_collection_update(scene: bpy.types.Scene, update: bpy.types.DepsgraphUpdate):
"""
Handles update to collections.
:param scene: Current scene.
:param update: Current update.
"""
collection: bpy.types.Collection = update.id.original
# check if we are inside a WMO collection.
if collection.wow_wmo.enabled:
if collection.name not in scene.collection.children:
collection.wow_wmo.enabled = False
return
# verify integrity of child
else:
SpecialCollection.verify_root_collection_integrity(collection, WMO_SPECIAL_COLLECTION_TYPES)
DoodadSetsCollection.verify_doodad_sets_collection_integrity(scene, collection)
return
for col_type in WMO_SPECIAL_COLLECTION_TYPES:
if col_type.handle_collection_if_matched(scene, update, WMO_SPECIAL_COLLECTION_TYPES):
break
def handle_object_update(update: bpy.types.DepsgraphUpdate):
"""
Handles update to objects.
:param scene: Current scene.
:param update: Current update.
"""
for obj_type in WMO_CUSTOM_OBJECT_TYPES:
if obj_type.handle_object_if_matched(update):
break
@persistent
def on_depsgraph_update(scene: bpy.types.Scene, depsgraph: bpy.types.Depsgraph):
"""
Called on every update to depsgraph.
:param scene: Current scene.
:param depsgraph: Updated depsgraph.
"""
if scene.wow_scene.type != WoWSceneTypes.WMO.name or DepsgraphLock().status:
return
with DepsgraphLock():
for update in depsgraph.updates:
if isinstance(update.id, bpy.types.Object):
handle_object_update(update)
elif isinstance(update.id, bpy.types.Collection):
handle_collection_update(scene, update)
elif isinstance(update.id, bpy.types.Material):
handle_material_update(update)
MessageStack().invoke_message_box(icon='ERROR')
def register():
bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
def unregister():
bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
@@ -0,0 +1,28 @@
from bpy.types import Menu
class VIEW3D_MT_wmo_doodad_actions(Menu):
bl_label = "Doodad Actions"
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
pie.operator("scene.wow_doodads_bake_color", text='Bake Color', icon='SHADING_RENDERED')
pie.operator("scene.wow_doodad_set_color", text='Set Color', icon='SHADING_SOLID')
op = pie.operator("scene.wow_doodad_set_template_action", text='Template Select', icon='PMARKER_ACT')
op.action = 'SELECT'
op = pie.operator("scene.wow_doodad_set_template_action", text='Template Replace', icon='FILE_REFRESH')
op.action = 'REPLACE'
op = pie.operator("scene.wow_doodad_set_template_action", text='Template Delete', icon='CANCEL')
op.action = 'DELETE'
op = pie.operator("scene.wow_doodad_set_template_action", text='Template Rotate', icon='LOOP_FORWARDS')
op.action = 'ROTATE'
op = pie.operator("scene.wow_doodad_set_template_action", text='Template Resize', icon='FULLSCREEN_ENTER')
op.action = 'RESIZE'
@@ -0,0 +1,14 @@
from bpy.types import Menu
class VIEW3D_MT_wmo_group_actions(Menu):
bl_label = "Group Actions"
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
pie.operator("scene.wow_wmo_generate_materials", text='Generate materials', icon='MATERIAL_DATA')
pie.operator("scene.wow_fill_textures", text='Fill texture paths', icon='SEQ_SPLITVIEW')
pie.operator("scene.wow_quick_collision", text='Quick collision', icon='MOD_TRIANGULATE')
@@ -0,0 +1,331 @@
import bpy
import bmesh
import traceback
import os
from bpy.types import Menu
from functools import partial
from ....ui.preferences import get_project_preferences
from ....utils.misc import load_game_data
from ...utils.wmv import wmv_get_last_texture
from ...utils.materials import load_texture
from ...bl_render import update_wmo_mat_node_tree, load_wmo_shader_dependencies
class WMO_OT_assign_material(bpy.types.Operator):
bl_idname = "mesh.wow_assign_material"
bl_label = "Assign material"
bl_description = "Assign selected material to selected polygons"
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
mat_name: bpy.props.StringProperty(default='Material')
action: bpy.props.EnumProperty(
name='Action',
items=(('NAME', "", ""),
('NEW', "", "")),
default='NAME'
)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'MESH' and context.object.mode == 'EDIT'
def execute(self, context):
try:
obj = context.object
mesh = obj.data
mat = bpy.data.materials[self.mat_name] if self.action == 'NAME' else bpy.data.materials.new(self.mat_name)
if self.action == 'NEW':
texture = context.scene.wow_last_selected_images[-1].pointer
mat.wow_wmo_material.path = texture.wow_wmo_texture.path
mat.wow_wmo_material.diff_texture_1 = texture
load_wmo_shader_dependencies()
update_wmo_mat_node_tree(mat)
mat_index = -1
for i, m in enumerate(mesh.materials):
if m is mat:
mat_index = i
break
if mat_index < 0:
mat_index = len(mesh.materials)
mesh.materials.append(mat)
bm = bmesh.from_edit_mesh(mesh)
for face in bm.faces:
if face.select:
face.material_index = mat_index
bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False)
except:
traceback.print_exc()
self.report({'ERROR'}, "Setting material failed.")
return {'CANCELLED'}
return {'FINISHED'}
class WMO_OT_import_texture_from_filepath(bpy.types.Operator):
bl_idname = "mesh.wow_import_texture_filepath"
bl_label = "Import texture from filepath"
bl_description = "Import a texture file from filepath and create material for it."
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'MESH' and context.object.mode == 'EDIT'
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
game_data = load_game_data()
if not game_data:
self.report({'ERROR'}, "Importing texture failed. Game data was not loaded.")
return {'CANCELLED'}
path = game_data.traverse_file_path(bpy.path.abspath(self.filepath))
if not path:
self.report({'INFO'}, "Texture was not found in WoW file system. Set the path in material manually.")
texture = bpy.data.images.load(bpy.path.abspath(self.filepath))
texture.wow_wmo_texture.path = path if path else self.filepath
texture.name = os.path.basename(self.filepath)
mat = bpy.data.materials.new(name=texture.name)
mat.wow_wmo_material.diff_texture_1 = texture
load_wmo_shader_dependencies()
update_wmo_mat_node_tree(mat)
global display_material_select_pie
display_material_select_pie = False
context.scene.wow_cur_image = texture
display_material_select_pie = True
bpy.ops.mesh.wow_assign_material(mat_name=mat.name, action='NAME')
return {'FINISHED'}
class WMO_OT_import_texture_from_wmv(bpy.types.Operator):
bl_idname = "mesh.wow_import_texture_wmv"
bl_label = "Import texture from WoWModelViewer"
bl_description = "Import a texture file from WMV and create material for it."
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'MESH' and context.object.mode == 'EDIT'
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'}
path = wmv_get_last_texture()
if not path:
self.report({'ERROR'}, "WMV 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)
mat = bpy.data.materials.new(name=path.split('\\')[-1][:-4] + '.PNG')
mat.wow_wmo_material.diff_texture_1 = texture
load_wmo_shader_dependencies()
update_wmo_mat_node_tree(mat)
# slot = context.scene.wow_wmo_root_elements.materials.add()
# slot.pointer = mat
global display_material_select_pie
display_material_select_pie = False
context.scene.wow_cur_image = texture
display_material_select_pie = True
bpy.ops.mesh.wow_assign_material(mat_name=mat.name, action='NAME')
return {'FINISHED'}
class WMO_OT_select_texture_from_recent(bpy.types.Operator):
bl_idname = "mesh.wow_select_texture_from_recent"
bl_label = "Select texture"
bl_description = "Select recently used texture"
bl_options = {'REGISTER', 'INTERNAL'}
index: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'MESH' and context.object.mode == 'EDIT'
def execute(self, context):
context.scene.wow_cur_image = context.scene.wow_last_selected_images[self.index].pointer
return {'FINISHED'}
class VIEW3D_MT_wmo_select_texture(Menu):
bl_label = "Select texture"
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'MESH' and context.object.mode == 'EDIT'
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
box = pie.box()
box.label(text='Select texture', icon='VIEWZOOM')
row = box.row()
row.scale_x = 0.5
row.scale_y = 0.5
row.template_ID_preview(context.scene, 'wow_cur_image', hide_buttons=True, rows=5, cols=5)
if len(context.scene.wow_last_selected_images):
box = pie.box()
row = box.row()
for i, texture in enumerate(context.scene.wow_last_selected_images):
col = row.column()
col.template_icon(row.icon(texture.pointer), scale=5.0)
op = col.operator("mesh.wow_select_texture_from_recent", text='', icon='ADD')
op.index = i
# pie.operator("mesh.wow_import_texture_wmv", text='Import from WMV', icon='ADD')
pie.operator("mesh.wow_import_texture_filepath", text='Import from file', icon='ADD')
def timer(override):
bpy.ops.wm.call_menu_pie(override, name="VIEW3D_MT_wmo_select_material")
def handle_last_selected_images(scene):
# clear invalid items
while True:
for i, tex in enumerate(scene.wow_last_selected_images):
if not tex.pointer:
scene.wow_last_selected_images.remove(i)
break
else:
break
# avoid duplicates and truncate the collection
for i, tex in enumerate(scene.wow_last_selected_images):
if tex.pointer == scene.wow_cur_image:
scene.wow_last_selected_images.remove(i)
break
else:
if len(scene.wow_last_selected_images) > 5:
scene.wow_last_selected_images.remove(0)
slot = scene.wow_last_selected_images.add()
slot.pointer = scene.wow_cur_image
scene.wow_cur_image = None
display_material_select_pie = True
def set_image(self, value):
if self.wow_cur_image:
global display_material_select_pie
handle_last_selected_images(self)
if display_material_select_pie:
bpy.app.timers.register(partial(timer, bpy.context.copy()), first_interval=0.0)
def get_more_materials_list(self, context):
scene = bpy.context.scene
materials = list([mat for mat in bpy.data.materials
if mat.wow_wmo_material.diff_texture_1 == scene.wow_last_selected_images[-1].pointer])
return list([(mat.name, mat.name, mat.name, 'MATERIAL', i) for i, mat in enumerate(materials[5:])]) if len(
materials) > 6 else []
def update_more_materials(self, context):
if self.more_materials:
bpy.ops.mesh.wow_assign_material(mat_name=self.more_materials, action='NAME')
class VIEW3D_MT_wmo_select_material(Menu):
bl_label = "Select material"
def draw(self, context):
scene = bpy.context.scene
layout = self.layout
pie = layout.menu_pie()
op = pie.operator("mesh.wow_assign_material", text='New material', icon='ADD')
op.action = 'NEW'
materials = list([mat for mat in bpy.data.materials
if mat.wow_wmo_material.diff_texture_1 == scene.wow_last_selected_images[-1].pointer])
if len(materials) > 6:
for mat in materials[:5]:
op = pie.operator("mesh.wow_assign_material", text=mat.pointer.name, icon='MATERIAL')
op.mat_name = mat.pointer.name
op.action = 'NAME'
box = pie.box()
box.prop(scene, "more_materials", text='More', icon='MATERIAL')
else:
for mat in materials:
op = pie.operator("mesh.wow_assign_material", text=mat.pointer.name, icon='MATERIAL')
op.mat_name = mat.pointer.name
op.action = 'NAME'
class ImagePointerPropertyGroup(bpy.types.PropertyGroup):
pointer: bpy.props.PointerProperty(type=bpy.types.Image)
def register():
bpy.types.Scene.wow_cur_image = bpy.props.PointerProperty(type=bpy.types.Image, update=set_image)
bpy.types.Scene.wow_last_selected_images = bpy.props.CollectionProperty(type=ImagePointerPropertyGroup)
bpy.types.Scene.more_materials = bpy.props.EnumProperty( name='More'
, items=get_more_materials_list
, update=update_more_materials
)
def unregister():
del bpy.types.Scene.more_materials
del bpy.types.Scene.wow_last_selected_images
del bpy.types.Scene.wow_cur_image
@@ -0,0 +1,45 @@
import bpy
from ..custom_objects import *
class WMO_OT_pie_menu_manager(bpy.types.Operator):
bl_idname = 'wm.wmo_pie_menu_manager'
bl_label = 'Pie Menu Manager'
bl_options = {'INTERNAL', 'UNDO'}
def invoke(self, context, event):
if not context.object:
return {'FINISHED'}
if context.object.type == 'MESH':
if WoWWMOGroup.match(context.object):
if context.object.mode == 'EDIT':
bpy.ops.wm.call_menu_pie(name='VIEW3D_MT_wmo_select_texture')
elif context.object.mode == 'OBJECT':
bpy.ops.wm.call_menu_pie(name='VIEW3D_MT_wmo_group_actions')
elif WoWWMODoodad.match(context.object):
bpy.ops.wm.call_menu_pie(name='VIEW3D_MT_wmo_doodad_actions')
elif WoWWMOPortal.match(context.object):
bpy.ops.wm.call_menu_pie(name='VIEW3D_MT_wmo_portal_actions')
return {'FINISHED'}
keymap = None
def register():
global keymap
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D', region_type='WINDOW')
kmi = km.keymap_items.new("wm.wmo_pie_menu_manager", type='Q', value='PRESS', shift=True)
keymap = km, kmi
def unregister():
global keymap
km, kmi = keymap
km.keymap_items.remove(kmi)
@@ -0,0 +1,14 @@
from bpy.types import Menu
from .... import ui_icons
class VIEW3D_MT_wmo_portal_actions(Menu):
bl_label = "Portal Actions"
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
pie.operator("scene.wow_set_portal_dir_alg", text='Set portal direction', icon='INDIRECT_ONLY_ON')
pie.operator("scene.wow_bake_portal_relations", text='Bake portal relations', icon='LIBRARY_DATA_DIRECT')
@@ -0,0 +1,381 @@
import bpy
import mathutils
from ..panels.toolbar import switch_doodad_set, get_doodad_sets
from ...utils.doodads import import_doodad
from ...utils.wmv import wmv_get_last_m2, wow_export_get_last_m2
from ....ui.preferences import get_project_preferences
from ....utils.misc import find_nearest_object
from ....third_party.tqdm import tqdm
from ..custom_objects import WoWWMODoodad, WoWWMOGroup
from ...ui.collections import get_wmo_collection, get_current_wow_model_collection, get_or_create_collection, DoodadSetsCollection, get_wmo_groups_list
from ...ui.enums import SpecialCollections
class WMO_OT_wmv_import_doodad_from_wmv(bpy.types.Operator):
bl_idname = 'scene.wow_wmo_import_doodad_from_wmv'
bl_label = 'Import last M2 from WMV'
bl_description = 'Import last M2 from WoW Model Viewer'
bl_options = {'REGISTER', 'UNDO'}
def get_active_doodad_collection(self, scene : bpy.types.Scene) -> bpy.types.Collection | None:
active_coll = bpy.context.collection
active_set_collection : bpy.types.Collection = None
if DoodadSetsCollection.is_doodad_set_collection(scene, active_coll):
active_set_collection = active_coll
else:
# if the current selected collection isn't a doodadset coll, import to DefaultGlobal
wmo_model_collection = get_current_wow_model_collection(bpy.context.scene, 'wow_wmo')
if wmo_model_collection:
for set_collection in get_wmo_collection(scene, SpecialCollections.Doodads).children:
DoodadSetsCollection.verify_doodad_sets_collection_integrity(scene, wmo_model_collection)
if set_collection.name.startswith("Set_$DefaultGlobal"):
active_set_collection = set_collection
self.report({'INFO'}, "No Doodad Set collection selected, importing it to [Set_$DefaultGlobal].")
else:
self.report({'ERROR'}, "Failed to find an active WMO model collection.")
if not active_set_collection or active_set_collection is None:
self.report({'ERROR'}, "Failed to import doodad. No active doodad set is selected."
"Select the doodad set to import the doodad to in the WMO panel")
return None
return active_set_collection
def execute(self, context):
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()
cache_path = get_project_preferences().cache_dir_path
active_set_collection = self.get_active_doodad_collection(context.scene)
if active_set_collection is None:
return {'FINISHED'}
if not m2_path:
self.report({'ERROR'}, "WoW Model Viewer log contains no model entries."
"Make sure to use compatible WMV version or open an .m2 there.")
return {'CANCELLED'}
obj = import_doodad(m2_path, cache_path)
obj.location = context.scene.cursor.location
active_set_collection.objects.link(obj)
context.view_layer.objects.active = obj
obj.wow_wmo_doodad.color = (1, 1, 1, 1)
return {'FINISHED'}
class WMO_OT_doodads_bake_color(bpy.types.Operator):
bl_idname = "scene.wow_doodads_bake_color"
bl_label = "Bake doodads color"
bl_description = "Bake doodads colors from nearby vertex color values"
bl_options = {'UNDO', 'REGISTER'}
tree_map = {}
@staticmethod
def get_object_radius(obj):
corner_min = [32767, 32767, 32767]
corner_max = [0, 0, 0]
mesh = obj.data
for vertex in mesh.vertices:
for i in range(3):
corner_min[i] = min(corner_min[i], vertex.co[i])
corner_max[i] = max(corner_max[i], vertex.co[i])
result = (mathutils.Vector(corner_min) - mathutils.Vector(corner_max))
return (abs(result.x) + abs(result.y) + abs(result.z)) / 3
def gen_doodad_color(self, obj, group, wmo_model_collection):
mesh = group.data
if 'Col' not in mesh.color_attributes:
return 0.5, 0.5, 0.5, 1.0
radius = self.get_object_radius(obj)
colors = []
kd_tree = self.tree_map.get(group.name)
if not kd_tree:
kd_tree = mathutils.kdtree.KDTree(len(mesh.polygons))
for index, poly in enumerate(mesh.polygons):
kd_tree.insert(group.matrix_world @ poly.center, index)
kd_tree.balance()
self.tree_map[group.name] = kd_tree
polygons = kd_tree.find_range(obj.location, radius)
if not polygons:
polygons.append(kd_tree.find(obj.location))
color_layer = mesh.color_attributes["Col"]
is_corner_domain = (color_layer.domain == 'CORNER')
for poly in polygons:
if is_corner_domain:
for loop_index in mesh.polygons[poly[1]].loop_indices:
# colors.append(mesh.vertex_colors['Col'].data[loop_index].color)
colors.append(color_layer.data[loop_index].color)
else:
for vertex_index in mesh.polygons[poly[1]].vertices:
colors.append(color_layer.data[vertex_index].color)
if not colors:
return 0.5, 0.5, 0.5, 1.0
final_color = mathutils.Vector((0, 0, 0, 0))
for color in colors:
final_color += mathutils.Vector(color)
final_color = final_color / len(colors)
flags = wmo_model_collection.wow_wmo.flags
if "2" in flags and WoWWMOGroup.is_indoor(group):
final_color += mathutils.Vector(tuple([c / 2 for c in wmo_model_collection.wow_wmo.ambient_color]))
return final_color
def execute(self, context):
wmo_model_collection = get_current_wow_model_collection(bpy.context.scene, 'wow_wmo')
if wmo_model_collection:
doodad_counter = 0
groups = [obj for obj in get_wmo_groups_list(bpy.context.scene)]
for index, obj in enumerate(tqdm(bpy.context.selected_objects, desc='Baking doodad colors', ascii=True)):
if WoWWMODoodad.match(obj):
doodad_counter += 1
group = find_nearest_object(obj, groups)
if not group:
self.report({'ERROR'}, "No WMO group found.")
continue
group.data.color_attributes.get(('Col'))
if '0' not in group.wow_wmo_group.flags or not group.data.color_attributes.get('Col'):
self.report({'ERROR'}, "Nearest group object({}) of doodad {} does not have vertex colors.".format(group.name,obj))
continue
vertex_color = self.gen_doodad_color(obj, group, wmo_model_collection)
color = [x for x in vertex_color]
obj.wow_wmo_doodad.color = color
if doodad_counter:
self.report({'INFO'}, "Done baking colors to {} doodad instances.".format(doodad_counter))
else:
self.report({'ERROR'}, "No doodad instances found among selected objects.")
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class WMO_OT_doodad_set_color(bpy.types.Operator):
bl_idname = 'scene.wow_doodad_set_color'
bl_label = 'Set Doodad Color'
bl_description = "Set color to selected doodads"
bl_options = {'REGISTER', 'UNDO'}
color: bpy.props.FloatVectorProperty(
name='Color',
description='Color applied to doodads',
subtype='COLOR',
size=4
)
def draw(self, context):
self.layout.column().prop(self, "color")
def execute(self, context):
if not bpy.context.selected_objects:
self.report({'ERROR'}, "No objects selected.")
return {'FINISHED'}
success = False
for obj in bpy.context.selected_objects:
if WoWWMODoodad.match(obj):
obj.wow_wmo_doodad.color = self.color
success = True
if success:
self.report({'INFO'}, "Successfully assigned color to selected doodads.")
else:
self.report({'ERROR'}, "No doodads found among selected objects.")
return {'FINISHED'}
class WMO_OT_doodadset_template_action(bpy.types.Operator):
bl_idname = 'scene.wow_doodad_set_template_action'
bl_label = 'Template action'
bl_description = 'Apply an action to all instances of selected object on the scene'
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
action: bpy.props.EnumProperty(
items=[
('SELECT', "Select", "Rotate all instances of selected doodads", 'PMARKER_ACT', 0),
('REPLACE', "Replace", "Replace all instances of selected doodads with last M2 from WMV", 'FILE_REFRESH', 1),
('RESIZE', "Resize", "Resize all instances of selected doodads", 'FULLSCREEN_ENTER', 2),
('DELETE', "Delete", "Delete all instances of selected doodads", 'CANCEL', 3),
('ROTATE', "Rotate", "Rotate all instances of selected doodads", 'LOOP_FORWARDS', 4)],
default='SELECT'
)
scale: bpy.props.FloatProperty(
name="Scale",
description="Scale applied to doodads",
min=0.01,
max=20,
default=1
)
rotation: bpy.props.FloatVectorProperty(
name="Rotation",
default=(0, 0, 0, 0),
size=4
)
@classmethod
def poll(cls, context):
return True
def draw(self, context):
col = self.layout.column()
col.prop(self, "action", expand=True)
if self.action == 'RESIZE':
col.prop(self, "scale")
elif self.action == 'ROTATE':
col.prop(self, "rotation")
def execute(self, context):
target = None
active = bpy.context.view_layer.objects.active
if active and active.wow_wmo_doodad:
target = active.wow_wmo_doodad.path
else:
self.report({'ERROR'}, "Template functions require an active object.")
return {'CANCELLED'}
selected_only = False
if len(bpy.context.selected_objects) > 1:
selected_only = True
selected_objects = bpy.context.selected_objects.copy()
objects_to_select = []
success = False
if target:
new_obj = None
if self.action == 'REPLACE':
if not bpy.data.is_saved:
self.report({'ERROR'}, "Saved your blendfile first.")
return {'FINISHED'}
if not hasattr(bpy, "wow_game_data"):
self.report({'ERROR'}, "Connect to game data first.")
return {'FINISHED'}
bpy.ops.scene.wow_wmo_import_doodad_from_wmv()
new_obj = bpy.context.view_layer.objects.active
for obj in bpy.context.scene.objects:
is_selected = obj in selected_objects if selected_only else True
if obj.wow_wmo_doodad.path == target and is_selected:
if self.action == 'REPLACE':
location = obj.location
rotation = obj.rotation_quaternion
scale = obj.scale
parent = obj.parent
color = obj.wow_wmo_doodad.color
flags = obj.wow_wmo_doodad.flags
bpy.data.objects.remove(obj, do_unlink=True)
obj = new_obj.copy()
bpy.context.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.location = location
obj.rotation_mode = 'QUATERNION'
obj.rotation_quaternion = rotation
obj.scale = scale
obj.parent = parent
obj.wow_wmo_doodad.color = color
obj.wow_wmo_doodad.flags = flags
objects_to_select.append(obj)
elif self.action == 'RESIZE':
obj.scale *= self.scale
obj.select_set(True)
elif self.action == 'DELETE':
bpy.data.objects.remove(obj, do_unlink=True)
elif self.action == 'ROTATE':
obj.rotation_mode = 'QUATERNION'
for i, _ in enumerate(self.rotation):
obj.rotation_quaternion[i] += self.rotation[i]
elif self.action == 'SELECT':
obj.select_set(True)
success = True
for ob in objects_to_select:
ob.select_set(True)
if new_obj:
bpy.data.objects.remove(new_obj, do_unlink=True)
if success:
self.report({'INFO'}, "Template action applied.")
return {'FINISHED'}
else:
self.report({'ERROR'}, "No doodad is selected.")
return {'FINISHED'}
@@ -0,0 +1,32 @@
import bpy
from ...utils.fogs import create_fog_object
from ...ui.collections import get_wmo_collection, SpecialCollections
class WMO_OT_add_fog(bpy.types.Operator):
bl_idname = 'scene.wow_add_fog'
bl_label = 'Add fog'
bl_description = 'Add a WoW fog object to the scene'
def execute(self, context):
scn = context.scene
fog_collection = get_wmo_collection(scn, SpecialCollections.Fogs)
if not fog_collection:
self.report({'WARNING'}, "Can't add WMO Fog: No WMO Object Collection found in the scene.")
return {'FINISHED'}
fog_obj = create_fog_object()
# applying object properties
fog_obj.wow_wmo_fog.enabled = True
fog_collection.objects.link(fog_obj)
bpy.context.view_layer.objects.active = fog_obj
fog_obj.scale = (5.0, 5.0, 5.0) # default size to 5
fog_obj.wow_wmo_fog.color2 = (0.0, 0.0, 1.0) # set underwater color as blue
self.report({'INFO'}, "Successfully created WoW fog: " + fog_obj.name)
return {'FINISHED'}
@@ -0,0 +1,56 @@
import bpy
import traceback
from ....ui.preferences import get_project_preferences
from ...import_wmo import import_wmo_to_blender_scene_gamedata
from ...utils.wmv import wmv_get_last_wmo, wow_export_get_last_wmo, noggit_red_get_last_wmo
from ....utils.misc import load_game_data
class WMO_OT_import_last_wmo_from_wmv(bpy.types.Operator):
bl_idname = "scene.wow_import_last_wmo_from_wmv"
bl_label = "Load last WMO from preferred import method"
bl_description = "Load last WMO 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:
wmo_path = wmv_get_last_wmo()
elif project_preferences.import_method == 'WowExport':
if project_preferences.wow_export_path:
wmo_path = wow_export_get_last_wmo()
elif project_preferences.import_method == 'NoggitRed':
if project_preferences.noggit_red_path:
wmo_path = noggit_red_get_last_wmo()
if not wmo_path:
self.report({'ERROR'}, """Log contains no WMO entries.
Make sure to use compatible WMV version or WoW.Export and open a .wmo there.""")
return {'CANCELLED'}
try:
import_wmo_to_blender_scene_gamedata(wmo_path, bpy.context.scene.wow_scene.version)
except:
traceback.print_exc()
self.report({'ERROR'}, "Failed to import model.")
return {'CANCELLED'}
self.report({'INFO'}, "Done importing WMO object to scene.")
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
@@ -0,0 +1,40 @@
import bpy
from ...ui.collections import get_wmo_collection, SpecialCollections
class WMO_OT_add_light(bpy.types.Operator):
bl_idname = 'scene.wow_add_light'
bl_label = 'Add light'
bl_description = 'Add a WoW light object to the scene'
def execute(self, context):
scn = context.scene
light_collection = get_wmo_collection(scn, SpecialCollections.Lights)
if not light_collection:
self.report({'WARNING'}, "Can't add WMO Light: No WMO Object Collection found in the scene.")
return {'FINISHED'}
light = bpy.data.lights.new(name='WoW Light', type='POINT')
obj = bpy.data.objects.new('WoW Light', light)
light.color = (1.0, 0.565, 0.0)
light.energy = 1.0
obj.wow_wmo_light.enabled = True
obj.wow_wmo_light.use_attenuation = True
obj.wow_wmo_light.color = light.color # set yellow as default
obj.wow_wmo_light.color_alpha = 1.0
obj.wow_wmo_light.intensity = light.energy
# light.falloff_type = 'INVERSE_LINEAR'
# move lights to collection
light_collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
bpy.data.objects[obj.name].select_set(True)
obj.location = bpy.context.scene.cursor.location
self.report({'INFO'}, "Successfully created WoW light: " + obj.name)
return {'FINISHED'}
@@ -0,0 +1,810 @@
import bpy
import bmesh
import os
from math import cos, sin, tan, radians
from time import time
from mathutils import Vector
from mathutils.bvhtree import BVHTree
from bpy_extras import view3d_utils
from ....addon_common.cookiecutter.cookiecutter import CookieCutter
from ....addon_common.common import ui
from ....addon_common.common.utils import delay_exec
from ....addon_common.common.drawing import Drawing
from ....addon_common.common.boundvar import BoundInt, BoundFloat, BoundBool
from ....addon_common.common.ui_styling import load_defaultstylings
from ....addon_common.common.globals import Globals
from ..handlers import DepsgraphLock
from .. import handlers
from ...ui.collections import get_wmo_collection, SpecialCollections
def angled_vertex(origin: Vector, pos: Vector, angle: float, orientation: float) -> float:
return origin.z + ((pos.x - origin.x) * cos(orientation) + (pos.y - origin.y) * sin(orientation)) * tan(angle)
def get_median_point(bm: bmesh.types.BMesh) -> Vector:
selected_vertices = [v for v in bm.verts if v.select]
f = 1 / len(selected_vertices)
median = Vector((0, 0, 0))
for vert in selected_vertices:
median += vert.co * f
return median
def align_vertices(bm : bmesh.types.BMesh, mesh : bpy.types.Mesh, median : Vector, angle : float, orientation : float):
for vert in bm.verts:
if vert.select:
vert.co[2] = angled_vertex(median, vert.co, radians(angle), radians(orientation))
bmesh.update_edit_mesh(mesh, loop_triangles=True, destructive=True)
def reload_stylings():
load_defaultstylings()
path = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'ui', 'ui.css')
try:
Globals.ui_draw.load_stylesheet(path)
except AssertionError as e:
# TODO: show proper dialog to user here!!
print('could not load stylesheet "%s"' % path)
print(e)
Globals.ui_document.body.dirty('Reloaded stylings', children=True)
Globals.ui_document.body.dirty_styling()
Globals.ui_document.body.dirty_flow()
event_keymap = {
'ONE' : 0,
'TWO' : 1,
'THREE': 2,
'FOUR': 3,
'FIVE': 4,
'SIX': 5,
'SEVEN': 6,
'EIGHT': 7,
'NUMPAD_1': 0,
'NUMPAD_2': 1,
'NUMPAD_3': 2,
'NUMPAD_4': 3,
'NUMPAD_5': 4,
'NUMPAD_6': 5,
'NUMPAD_7': 6,
'NUMPAD_8': 7,
}
# some settings container
options = {}
options["variable_1"] = 10.0
options["variable_3"] = True
options["x_size"] = 10
options["y_size"] = 10
options["brush_size"] = 2
flags = {}
flags["flag_1"] = True
flags["flag_2"] = False
flags["flag_3"] = False
flag_checkboxes = []
class WMO_OT_edit_liquid(CookieCutter, bpy.types.Operator):
bl_idname = "wow.liquid_edit_mode"
bl_label = "Edit WoW Liquid"
default_keymap = {
'cancel': {'ESC', 'TAB'},
'grab': 'G',
'rotate': 'R',
'equalize': 'E',
'flag': 'F',
'paint': {'LEFTMOUSE', 'SHIFT+LEFTMOUSE'}
}
@property
def variable_2_gs(self):
return getattr(self, '_var_cut_count_value', 0)
@variable_2_gs.setter
def variable_2_gs(self, v):
if self.variable_2 == v: return
self.variable_2 = v
# if self.variable_2.disabled: return
def start(self):
self.init_loc = 0.0
self.speed_modifier = 1.0
self.orientation = 0.0
self.angle = 0.0
self.median = Vector((0, 0, 0))
self.color_type = 'TEXTURE'
self.shading_type = 'SOLID'
self.selected_verts = {}
self.viewports = []
self.init_time = time()
self.obj = self.context.object
self.mesh = self.context.object.data
self.active_tool = 'select'
DepsgraphLock().push()
bpy.ops.mesh.select_mode(bpy.context.copy(), type='VERT', action='ENABLE', use_extend=True)
bpy.ops.mesh.select_mode(bpy.context.copy(), type='EDGE', action='ENABLE', use_extend=True)
bpy.ops.mesh.select_mode(bpy.context.copy(), type='FACE', action='ENABLE', use_extend=True)
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.wm.tool_set_by_id(bpy.context.copy(), name="builtin.select_box") # force a benign select tool
# create a bmesh to operate on
self.bm = bmesh.from_edit_mesh(self.context.object.data)
self.bm.verts.ensure_lookup_table()
# create BVH tree for ray_casting
self.bvh_tree = BVHTree.FromBMesh(self.bm)
# store viewports
self.viewports = [a for a in self.context.screen.areas if a.type == 'VIEW_3D']
for viewport in self.viewports:
self.color_type = viewport.spaces[0].shading.color_type
self.shading_type = viewport.spaces[0].shading.type
viewport.spaces[0].shading.type = 'SOLID'
viewport.spaces[0].shading.color_type = 'VERTEX'
# setup UI variables
self.tools = {
"select": ("Select (TODO : Select tool)", "select.png", "Select liquid area"),
"grab": ("Raise / Lower (G)", "raise_lower.png", "Raise \ Lower liquid surface"),
"rotate": ("Rotate (R)", "rotate.png", "Rotate liquid area"),
"equalize": ("Equalize (E)", "equalize.png", "Equalize liquid level"),
"flag": ("Edit flags (F)", "flags.png", "Mark flags on the liquid grid. Hold Shift to unmark"),
}
self.variable_1 = BoundFloat('''options['variable_1']''', min_value=0.5, max_value=15.5)
self.variable_2 = BoundInt('''self.variable_2_gs''', min_value=0, max_value=10)
self.variable_3 = BoundBool('''options['variable_3']''')
self.x_size = BoundInt('''options['x_size']''', min_value=1, max_value=100000) # uint32, is there a limit ?
self.y_size = BoundInt('''options['y_size']''', min_value=1, max_value=100000)
self.brush_size = BoundInt('''options['brush_size']''', min_value=1, max_value=4)
self.flag_1 = BoundBool('''flags['flag_1']''')
self.flag_2 = BoundBool('''flags['flag_2']''')
self.flag_3 = BoundBool('''flags['flag_3']''')
self.blender_ui_set()
self.setup_ui()
def blender_ui_set(self):
self.viewaa_simplify()
self.manipulator_hide()
self._space.show_gizmo = True
self.panels_hide()
#self.region_darken()
def update_ui(self):
self.ui_main.dirty('update', parent=True, children=True)
def select_tool(self, action):
tool_id = "tool-{}".format(action)
e = self.document.body.getElementById('tool-{}'.format(tool_id))
if e: e.checked = True
self.active_tool = action
self.update_ui()
def setup_ui(self):
reload_stylings()
self.ui_main = ui.framed_dialog(label='Liquid Editor',
resizable=None,
resizable_x=True,
resizable_y=False,
closeable=False,
moveable=True,
hide_on_close=True,
parent=self.document.body)
# tools
ui_tools = ui.div(id="tools", parent=self.ui_main)
def add_tool(action="", name="", icon="", title=""):
nonlocal ui_tools
nonlocal self
# must be a fn so that local vars are unique and correctly captured
lbl, img = name, icon
radio = ui.input_radio(id='tool-{}'.format(action), value=lbl.lower(), title=title, name="tool",
classes="tool", checked=False, parent=ui_tools)
radio.add_eventListener('on_input', delay_exec('''if radio.checked: self.select_tool("{}")'''.format(action)))
ui.img(src=img, parent=radio, title=title)
ui.label(innerText=lbl, parent=radio, title=title)
for key, value in self.tools.items(): add_tool(action=key, name=value[0], icon=value[1], title=value[2])
# ui.button(label='ui.button', title='self.tool_action() method linked to button', parent=ui_tools,
# on_mouseclick=self.tool_action)
# create a collapsille container to hold a few variables
# container = ui.collapsible('ui.collapse container', parent=self.ui_main)
#
# i1 = ui.labeled_input_text(label='Sui.labeled_input_text',
# title='float property to BoundFLoat',
# value=self.variable_1)
#
# i2 = ui.labeled_input_text(label='ui.labled_input_text',
# title='integer property to BoundInt',
# value=self.variable_2)
#
# i3 = ui.input_checkbox(
# label='ui.input_checkbox',
# title='True/False property to BoundBool')
#
# container.builder([i1, i2, i3])
size_container = ui.collapsible('Liquid Size', parent=self.ui_main, collapsed=False)
j1 = ui.labeled_input_text(label='X subdivisions',
title='Amount of WoW liquid planes in a row. One plane is 4.1666625 in its radius.',
value=self.x_size)
j2 = ui.labeled_input_text(label='Y subdivisions',
title='Amount of WoW liquid planes in a row. One plane is 4.1666625 in its radius.',
value=self.y_size)
# j3 = ui.button(label='Apply size', on_mouseclick=self.set_grid_size)
j3 = ui.button(label='Apply size ( Not implemented yet)')
size_container.builder([j1, j2, j3])
self.get_grid_size()
flags_brush_size = ui.labeled_input_text(label='Flags brush size', parent=ui_tools,
title='Size of the brush used to paint flags, from 1 to 4. Also press K/shift+K to set/unset all the liquid.',
value=self.brush_size)
chk_flag_1 = ui.input_checkbox(
label='No Render flag', parent=ui_tools,
title='Set "No Render" as the flag to set/unset',
on_mouseclick=lambda: self.set_editable_flag(1),
value = self.flag_1)
chk_flag_2 = ui.input_checkbox(
label='Fishing flag', parent=ui_tools,
title='Set "Fishing" as the flag to set/unset',
on_mouseclick=lambda: self.set_editable_flag(2),
value = self.flag_2)
chk_flag_3 = ui.input_checkbox(
label='Fatigue Flag', parent=ui_tools,
title='Set "Fatigue" as the flag to set/unset',
on_mouseclick=lambda: self.set_editable_flag(3),
value = self.flag_3)
self.flag_checkboxes = [chk_flag_1, chk_flag_2, chk_flag_3]
chk_flag_1.checked = True
self.mesh.vertex_colors.get("flag_0").active = True
ui.button(label='Sculpt liquid', title="Sculpt the Liquid mesh, locked in Z edit.", parent=ui_tools,
on_mouseclick=self.activate_sculpt_mode)
def set_editable_flag(self, flag): # TODO : come up with a better solution
if flag == 1:
self.mesh.vertex_colors.get("flag_0").active = True
if self.flag_checkboxes[0].checked:
return
else:
self.flag_checkboxes[1].checked = False
self.flag_checkboxes[2].checked = False
elif flag == 2:
self.mesh.vertex_colors.get("flag_6").active = True
if self.flag_checkboxes[1].checked:
# self.flag_checkboxes[1].checked = True
return
else:
# self.flag_2.set(False)
self.flag_checkboxes[0].checked = False
# self.flag_3.set(False)
self.flag_checkboxes[2].checked = False
elif flag == 3:
self.mesh.vertex_colors.get("flag_7").active = True
if self.flag_checkboxes[2].checked:
# self.flag_checkboxes[2].checked = True
return
else:
self.flag_checkboxes[0].checked = False
self.flag_checkboxes[1].checked = False
def get_grid_size(self):
x_tiles = round(self.context.object.dimensions[0] / 4.1666625)
y_tiles = round(self.context.object.dimensions[1] / 4.1666625)
self.x_size.set(x_tiles)
self.y_size.set(y_tiles)
def set_grid_size(self):
start_vertex = 0
sum = 0
for vertex in self.context.object.data.vertices:
cur_sum = vertex.co[0] + vertex.co[1]
if cur_sum < sum:
start_vertex = vertex.index
sum = cur_sum
old_x_tiles = round(self.context.object.dimensions[0] / 4.1666625)
old_y_tiles = round(self.context.object.dimensions[1] / 4.1666625)
old_x_verts = old_x_tiles + 1
old_y_verts = old_y_tiles + 1
position = self.context.object.data.vertices[start_vertex].co.to_tuple()
if old_x_tiles == self.x_size.get() and old_y_tiles == self.y_size.get():
return
# create each missing vert (for in newmax tile - cur tile)
x_verts = self.x_size.get()+1
y_verts = self.y_size.get()+1
vertices = []
for y in range(old_y_verts, y_verts):
y_pos = position[1] + y * 4.1666625
for x in range(old_x_verts, x_verts):
x_pos = position[1] + x * 4.1666625
vertices.append((x_pos, y_pos, position[2]))
# calculate faces
indices = []
for y in range(self.y_size.get()):
for x in range(self.x_size.get()):
indices.append(y * x_verts + x)
indices.append(y * x_verts + x + 1)
indices.append((y + 1) * y_verts + x)
indices.append((y + 1) * y_verts + x + 1)
faces = []
for i in range(0, len(indices), 4):
faces.append((indices[i], indices[i + 1], indices[i + 3], indices[i + 2]))
bpy.ops.object.mode_set(mode='OBJECT')
print(len(vertices))
print(len(faces))
name = "_Liquid"
mesh = bpy.data.meshes.new(name)
# obj = bpy.data.objects.new(name, mesh)
#
# # create mesh from python data
mesh.from_pydata(vertices, [], faces)
mesh.update(calc_edges=True)
mesh.validate()
# self.bm.verts
bm = bmesh.new()
bm.from_mesh(mesh)
bpy.ops.object.mode_set(mode='EDIT')
bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
def should_pass_through(self, context, event):
# allow selection events to pass through
return True if event.type in {'A', 'B', 'C'} else False
def tool_action(self):
print('tool action')
return
def activate_sculpt_mode(self):
bpy.ops.object.mode_set(mode='SCULPT')
for viewport in self.viewports:
viewport.spaces[0].shading.type = self.shading_type
viewport.spaces[0].shading.color_type = self.color_type
self.done(cancel=False)
return
def activate_tool(self, name):
self.active_tool = name
e = self.document.body.getElementById('tool-{}'.format(self.active_tool))
if e: e.checked = True
def update_bmesh(self):
self.bm = bmesh.from_edit_mesh(self.context.object.data)
self.bm.verts.ensure_lookup_table()
def select_all_verts(self):
# for v in self.mesh:
# v.select = True
bpy.ops.mesh.select_all(action='SELECT')
def unselect_all_verts(self):
bpy.ops.mesh.select_all(action='DESELECT')
@CookieCutter.FSM_State('main', 'enter')
def enter_main(self):
self.update_bmesh()
@CookieCutter.FSM_State('main')
def modal_main(self):
self.context.area.tag_redraw()
Drawing.set_cursor('DEFAULT')
if self.actions.pressed('grab') or self.active_tool == 'grab':
bpy.ops.mesh.select_all(action='SELECT')
self.activate_tool('grab')
Drawing.set_cursor('MOVE_X')
self.init_loc = self.event.mouse_x
self.selected_verts = {vert: vert.co[2] for vert in self.bm.verts if vert.select}
return 'grab'
elif self.actions.pressed('rotate') or self.active_tool == 'rotate':
self.activate_tool('rotate')
self.report({'INFO'}, "Rotating vertices. Shift + Scroll - tilt | Alt + Scroll - rotate")
bpy.ops.mesh.select_all(action='SELECT')
self.selected_verts = {vert: vert.co[2] for vert in self.bm.verts if vert.select}
self.median = get_median_point(self.bm)
self.orientation = 0.0
self.angle = 0.0
return 'rotate'
elif self.actions.pressed('equalize') or self.active_tool == 'equalize':
bpy.ops.mesh.select_all(action='SELECT')
self.activate_tool('equalize')
return 'equalize'
elif self.actions.pressed('flag') or self.active_tool == 'flag':
bpy.ops.mesh.select_all(action='DESELECT')
self.activate_tool('flag')
return 'flag'
elif self.actions.pressed('cancel') and (time() - self.init_time) > 0.5:
bpy.ops.object.mode_set(mode='OBJECT')
for viewport in self.viewports:
viewport.spaces[0].shading.type = self.shading_type
viewport.spaces[0].shading.color_type = self.color_type
DepsgraphLock().pop()
self.done(cancel=False)
return 'finished'
else:
self.activate_tool('select')
# return 'select'
@CookieCutter.FSM_State('select')
def modal_select(self):
if self.active_tool != 'select':
return 'main'
if self.actions.pressed('cancel') and (time() - self.init_time) > 0.5:
bpy.ops.object.mode_set(mode='OBJECT')
for viewport in self.viewports:
viewport.spaces[0].shading.type = self.shading_type
viewport.spaces[0].shading.color_type = self.color_type
DepsgraphLock().DEPSGRAPH_UPDATE_LOCK = False
self.done(cancel=False)
return 'finished'
@CookieCutter.FSM_State('grab')
def modal_grab(self):
if self.active_tool != 'grab':
return 'main'
else:
self.activate_tool('grab')
# alter vertex height
if self.actions.mousemove:
fac = 10 if self.actions.shift else 30
for vert, height in self.selected_verts.items():
vert.co[2] = height + (self.event.mouse_x - self.init_loc) / fac
bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
# accept
if self.actions.event_type == 'LEFTMOUSE':
self.active_tool = 'select'
return 'main'
# cancel
elif self.actions.event_type == 'RIGHTMOUSE':
for vert, height in self.selected_verts.items():
vert.co[2] = height
bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
self.active_tool = 'select'
return 'main'
# switch state
for action in self.default_keymap.keys():
if self.actions.pressed(action):
self.update_bmesh()
self.activate_tool(action)
return action
return 'grab'
@CookieCutter.FSM_State('rotate')
def modal_rotate(self):
if self.active_tool != 'rotate':
return 'main'
Drawing.set_cursor('SCROLL_Y')
if self.actions.event_type == 'WHEELUPMOUSE':
if self.actions.shift:
self.angle = min(self.angle + 5, 89.9)
align_vertices(self.bm, self.context.object.data, self.median, self.angle, self.orientation)
elif self.actions.alt:
self.orientation += 10
if self.orientation > 360:
self.orientation -= 360
align_vertices(self.bm, self.context.object.data, self.median, self.angle, self.orientation)
elif self.actions.event_type == 'WHEELDOWNMOUSE':
if self.actions.shift:
self.angle = max(self.angle - 5, -89.9)
align_vertices(self.bm, self.context.object.data, self.median, self.angle, self.orientation)
elif self.actions.alt:
self.orientation -= 10
if self.orientation < 0:
self.orientation = 360 - self.orientation
align_vertices(self.bm, self.context.object.data, self.median, self.angle, self.orientation)
# accept
if self.actions.event_type == 'LEFTMOUSE':
self.active_tool = 'select'
return 'main'
# cancel
elif self.actions.event_type == 'RIGHTMOUSE':
for vert, height in self.selected_verts.items():
vert.co[2] = height
bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
self.active_tool = 'select'
return 'main'
# switch state
for action in self.default_keymap.keys():
if self.actions.pressed(action):
self.update_bmesh()
self.activate_tool(action)
return action
return 'rotate'
@CookieCutter.FSM_State('equalize')
def equalize(self):
median = get_median_point(self.bm)
for vert in self.bm.verts:
if vert.select:
vert.co[2] = median[2]
bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
self.report({'INFO'}, "Equalized vertex height")
self.active_tool = 'select'
return 'main'
@CookieCutter.FSM_State('flag')
def modal_flag(self):
if self.active_tool != 'flag':
return 'main'
Drawing.set_cursor('PAINT_BRUSH')
# if self.actions.event_type in event_keymap.keys():
# flag_number = event_keymap.get(self.actions.event_type, 0)
# layer = self.mesh.vertex_colors.get("flag_{}".format(flag_number))
# layer.active = True
layer = self.bm.loops.layers.color.active
color = (0, 0, 255, 255) if not self.actions.shift else (255, 255, 255, 255)
if self.actions.event_type == 'K':
bpy.ops.mesh.select_all(action='SELECT') # Remove when selecting tool is fixed
for face in self.bm.faces:
if face.select:
for loop in face.loops:
loop[layer] = color
bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
bpy.ops.mesh.select_all(action='DESELECT')
self.report({'INFO'}, "Flag unset" if self.actions.shift else "Flag set")
if not self.actions.released('paint'):
# TODO: radius brush
# get the context arguments
region = self.context.region
rv3d = self.context.region_data
coord = self.event.mouse_region_x, self.event.mouse_region_y
# get the ray from the viewport and mouse
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
ray_target = ray_origin + view_vector
ray_origin_obj = self.obj.matrix_world.inverted() @ ray_origin
ray_target_obj = self.obj.matrix_world.inverted() @ ray_target
ray_direction_obj = ray_target_obj - ray_origin_obj
# cast the ray
location, normal, face_index, distance = self.bvh_tree.ray_cast(ray_origin_obj, ray_direction_obj)
if face_index is not None:
color = (0, 0, 255, 255) if not self.actions.shift else (255, 255, 255, 255)
face = self.bm.faces[face_index]
############
# This is extremly innefficient, capped the variable at 3 or it lags too much.
# TODO : find a betetr way
faces_list = []
faces_list.append(face)
size_count = 1
while size_count < self.brush_size.get():
curr_faces = faces_list.copy()
for face in faces_list:
for vert in face.verts:
linked_faces = vert.link_faces
for linked_face in linked_faces:
curr_faces.append(linked_face)
size_count += 1
for curr_face in curr_faces:
faces_list.append(curr_face)
for face in faces_list:
for loop in face.loops:
loop[layer] = color
##############
# for loop in face.loops:
# loop[layer] = color
bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
if self.actions.event_type == 'RIGHTMOUSE':
self.active_tool = 'select'
return 'main'
else:
return 'flag'
class WMO_OT_add_liquid(bpy.types.Operator):
bl_idname = 'scene.wow_add_liquid'
bl_label = 'Add liquid'
bl_description = 'Add a WoW liquid plane'
bl_options = {'REGISTER', 'UNDO'}
x_planes: bpy.props.IntProperty(
name="X subdivisions:",
description="Amount of WoW liquid planes in a row. One plane is 4.1666625 in its radius.",
default=10,
min=1
)
y_planes: bpy.props.IntProperty(
name="Y subdivisions:",
description="Amount of WoW liquid planes in a column. One plane is 4.1666625 in its radius.",
default=10,
min=1
)
def execute(self, context):
liquid_collection = get_wmo_collection(context.scene, SpecialCollections.Liquids)
if not liquid_collection:
self.report({'WARNING'}, "Can't add WMO Liquid: No WMO Object Collection found in the scene.")
return {'FINISHED'}
bpy.ops.mesh.primitive_grid_add(x_subdivisions=self.x_planes,
y_subdivisions=self.y_planes,
size=4.1666625
)
water = bpy.context.view_layer.objects.active
bpy.ops.transform.resize(value=(self.x_planes, self.y_planes, 1.0))
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
water.name += "_Liquid"
mesh = water.data
water.wow_wmo_liquid.enabled = True
# move to collection
liquid_collection.objects.link(water)
bit = 1
counter = 0
while bit <= 0x80:
vc_layer = mesh.vertex_colors.new(name="flag_{}".format(counter))
# set flag 7 which is very likely related to swimming and not fishing (it's enabled in most liquids, even lava)
if bit == 0x40:
for poly in mesh.polygons:
for loop in poly.loop_indices:
vc_layer.data[loop].color = (0, 0, 255, 255)
counter += 1
bit <<= 1
water.hide_set(False if "4" in bpy.context.scene.wow_visibility else True)
self.report({'INFO'}, "Successfully created WoW liquid: {}".format(water.name))
return {'FINISHED'}
@@ -0,0 +1,234 @@
import bpy
import bmesh
import os
from ...bl_render import load_wmo_shader_dependencies, update_wmo_mat_node_tree
from ...utils.wmv import wmv_get_last_texture, wow_export_get_last_texture
from ....utils.misc import resolve_texture_path, resolve_outside_texture_path, load_game_data
from ...utils.materials import load_texture
from ....ui.preferences import get_project_preferences
from ...ui.handlers import DepsgraphLock
from ..custom_objects import WoWWMOGroup
class WMO_OT_generate_materials(bpy.types.Operator):
bl_idname = "scene.wow_wmo_generate_materials"
bl_label = "Generate WMO Materials"
bl_description = "Generate WMO materials."
bl_options = {'UNDO', 'REGISTER'}
@classmethod
def poll(cls, context):
return context.scene.wow_scene.type == 'WMO'
def execute(self, context):
load_wmo_shader_dependencies()
materials = []
if context.selected_objects:
for obj in context.selected_objects:
if not WoWWMOGroup.match(obj):
continue
materials.extend(obj.data.materials)
for mat in materials:
tex = None
if mat.use_nodes:
for node in mat.node_tree.nodes:
if node.bl_idname == 'ShaderNodeTexImage':
tex = node.image
break
update_wmo_mat_node_tree(mat)
with DepsgraphLock():
# if context.scene.wow_wmo_root_elements.materials.find(mat.name) < 0:
if bpy.data.materials.find(mat.name) < 0:
mat.wow_wmo_material.diff_texture_1 = tex
return {'FINISHED'}
class WMO_OT_material_assign(bpy.types.Operator):
bl_idname = "object.wow_wmo_material_assign"
bl_label = "Assign WMO Material"
bl_description = "Assign WMO material to selected faces."
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
def execute(self, context):
mesh = context.view_layer.objects.active.data
bm = bmesh.from_edit_mesh(mesh)
# mat = context.scene.wow_wmo_root_elements.materials[context.scene.wow_wmo_root_elements.cur_material]
# TODO : cur material in new system?
mat = bpy.data.materials[cur_material]
# if not mat.pointer:
# self.report({'ERROR'}, "Cannot assign an empty material")
# return {'CANCELLED'}
mat_index = mesh.materials.find(mat.name)
if mat_index < 0:
mat_index = len(mesh.materials)
mesh.materials.append(mat)
for face in bm.faces:
if not face.select:
continue
face.material_index = mat_index
bmesh.update_edit_mesh(mesh)
return {'FINISHED'}
class WMO_OT_material_select(bpy.types.Operator):
bl_idname = "object.wow_wmo_material_select"
bl_label = "Select WMO Material"
bl_description = "Select WMO material to selected faces."
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
def execute(self, context):
mesh = context.view_layer.objects.active.data
bm = bmesh.from_edit_mesh(mesh)
#mat = context.scene.wow_wmo_root_elements.materials[context.scene.wow_wmo_root_elements.cur_material]
# TODO : cur material in new system
mat = bpy.data.materials[cur_material]
# if not mat.pointer:
# self.report({'ERROR'}, "Cannot select an empty material")
# return {'CANCELLED'}
mat_index = mesh.materials.find(mat.name)
for face in bm.faces:
if face.material_index == mat_index:
face.select = True
bmesh.update_edit_mesh(mesh)
return {'FINISHED'}
class WMO_OT_material_deselect(bpy.types.Operator):
bl_idname = "object.wow_wmo_material_deselect"
bl_label = "Deselect WMO Material"
bl_description = "Deselect WMO material to selected faces."
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
def execute(self, context):
mesh = context.view_layer.objects.active.data
bm = bmesh.from_edit_mesh(mesh)
# mat = context.scene.wow_wmo_root_elements.materials[context.scene.wow_wmo_root_elements.cur_material]
# TODO
mat = bpy.data.materials[cur_material]
# if not mat.pointer:
# self.report({'ERROR'}, "Cannot deselect an empty material")
# return {'CANCELLED'}
mat_index = mesh.materials.find(mat.name)
for face in bm.faces:
if face.material_index == mat_index:
face.select = False
bmesh.update_edit_mesh(mesh)
return {'FINISHED'}
class WMO_OT_fill_textures(bpy.types.Operator):
bl_idname = 'scene.wow_fill_textures'
bl_label = 'Fill textures'
bl_description = "Fill Texture field of WoW materials with paths from applied image"
bl_options = {'REGISTER'}
def execute(self, context):
for ob in filter(lambda o: WoWWMOGroup.match(o), bpy.context.selected_objects):
mesh = ob.data
for material in mesh.materials:
if not WoWWMOGroup.match(ob) :
continue
texture1 = material.wow_wmo_material.diff_texture_1
texture2 = material.wow_wmo_material.diff_texture_2
if not texture1 or texture1.type != 'IMAGE':
continue
t1_resolved_path = resolve_texture_path(texture1.filepath)
if t1_resolved_path is None:
t1_resolved_path = resolve_outside_texture_path(texture1.filepath)
texture1.wow_wmo_texture.path = t1_resolved_path
if not texture2 or texture2.type != 'IMAGE':
continue
if texture2 is not None:
t2_resolved_path = resolve_texture_path(texture2.filepath)
if t2_resolved_path is None:
t2_resolved_path = resolve_outside_texture_path(texture2.filepath)
texture2.wow_wmo_texture.path = t2_resolved_path
self.report({'INFO'}, "Done filling texture paths")
return {'FINISHED'}
class WMO_OT_import_texture(bpy.types.Operator):
bl_idname = "scene.wow_wmo_texture_import"
bl_label = "Import WoW Texture"
bl_description = "Import last texture from WoW Model Viewer as a WMO material.\n(Browse WMV in 'Images' mode, modeling textures are usualy in 'Dungeons' directory)"
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'}, "WMV 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)
mat = bpy.data.materials.new(name="T1_" + os.path.basename(path).replace('.blp', ''))
mat.wow_wmo_material.diff_texture_1 = texture
mat.wow_wmo_material.diff_color = (0.584314,0.584314,0.584314,1)
mat.wow_wmo_material.emissive_color = (0,0,0,1)
load_wmo_shader_dependencies()
update_wmo_mat_node_tree(mat)
print("Info: Successfully imported texture: " + mat.name)
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="Info: Successfully imported texture: " + mat.name, font_size=24, y_offset=67)
return {'FINISHED'}
@@ -0,0 +1,470 @@
import hashlib
import math
import random
import bpy
import io
import os
import bmesh
from ....utils.collections import get_current_wow_model_collection
from .... import PACKAGE_NAME
from ....utils.misc import load_game_data
from ....pywowlib.blp import PNG2BLP
# from ....pywowlib.io_utils.types import *
from ...ui.custom_objects import *
from ..collections import get_wmo_collection, SpecialCollections, get_wmo_groups_list
from ....ui.preferences import get_project_preferences
from ....third_party.tqdm import tqdm
class WMO_OT_add_scale(bpy.types.Operator):
bl_idname = 'scene.wow_add_scale_reference'
bl_label = 'Add scale'
bl_description = 'Add a WoW scale prop'
bl_options = {'REGISTER', 'UNDO'}
scale_type: bpy.props.EnumProperty(
name="Scale Type",
description="Select scale reference type",
items=[('HUMAN', "Human Scale (average)", ""),
('TAUREN', "Tauren Scale (thickest)", ""),
('TROLL', "Troll Scale (tallest)", ""),
('GNOME', "Gnome Scale (smallest)", "")
],
default='HUMAN'
)
def execute(self, context):
if self.scale_type == 'HUMAN':
bpy.ops.object.add(type='LATTICE')
scale_obj = bpy.context.object
scale_obj.name = "Human Scale"
scale_obj.dimensions = (0.582, 0.892, 1.989)
elif self.scale_type == 'TAUREN':
bpy.ops.object.add(type='LATTICE')
scale_obj = bpy.context.object
scale_obj.name = "Tauren Scale"
scale_obj.dimensions = (1.663, 1.539, 2.246)
elif self.scale_type == 'TROLL':
bpy.ops.object.add(type='LATTICE')
scale_obj = bpy.context.object
scale_obj.name = "Troll Scale"
scale_obj.dimensions = (1.116, 1.291, 2.367)
elif self.scale_type == 'GNOME':
bpy.ops.object.add(type='LATTICE')
scale_obj = bpy.context.object
scale_obj.name = "Gnome Scale"
scale_obj.dimensions = (0.362, 0.758, 0.991)
self.report({'INFO'}, "Successfully added " + self.scale_type + " scale")
return {'FINISHED'}
class WMO_OT_quick_collision(bpy.types.Operator):
bl_idname = 'scene.wow_quick_collision'
bl_label = 'Generate collision'
bl_description = 'Generate WoW collision equal to geometry of the selected objects'
bl_options = {'REGISTER', 'UNDO'}
leaf_size: bpy.props.IntProperty(
name="Node max size",
description="Max count of faces for a node in bsp tree on export. 0 = Dynamic based on face count",
default=0, min=0,
soft_max=1000
)
clean_up: bpy.props.BoolProperty(
name="Clean up",
description="Remove unreferenced vertex groups",
default=False
)
def execute(self, context):
success = False
selected_objects = bpy.context.selected_objects[:]
bpy.ops.object.select_all(action='DESELECT')
for ob in tqdm(selected_objects, desc='Generating collision', ascii=True):
if WoWWMOGroup.match(ob):
bpy.context.view_layer.objects.active = ob
if self.clean_up:
for vertex_group in ob.vertex_groups:
if vertex_group.name != ob.wow_wmo_vertex_info.vertex_group:
ob.vertex_groups.remove(vertex_group)
if ob.vertex_groups.get(ob.wow_wmo_vertex_info.vertex_group):
bpy.ops.object.vertex_group_set_active(group=ob.wow_wmo_vertex_info.vertex_group)
else:
new_vertex_group = ob.vertex_groups.new(name="Collision")
bpy.ops.object.vertex_group_set_active(group=new_vertex_group.name)
ob.wow_wmo_vertex_info.vertex_group = new_vertex_group.name
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.object.vertex_group_assign()
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
ob.wow_wmo_vertex_info.node_size = self.leaf_size
success = True
if success:
self.report({'INFO'}, "Successfully generated automatic collision for selected WMO groups")
return {'FINISHED'}
else:
self.report({'ERROR'}, "No WMO group objects found among selected objects")
return {'CANCELLED'}
class WMO_OT_select_entity(bpy.types.Operator):
bl_idname = 'scene.wow_wmo_select_entity'
bl_label = 'Select WMO entities'
bl_description = 'Select all WMO entities of given type'
bl_options = {'REGISTER', 'INTERNAL'}
entity: bpy.props.EnumProperty(
name="Entity",
description="Select WMO component entity objects",
items=[
("Outdoor", "Outdoor", ""),
("Indoor", "Indoor", ""),
("wow_wmo_portal", "Portals", ""),
("wow_wmo_liquid", "Liquids", ""),
("wow_wmo_fog", "Fogs", ""),
("wow_wmo_light", "Lights", ""),
("wow_wmo_doodad", "Doodads", ""),
("Collision", "Collision", "")
]
)
def execute(self, context):
# can optimise by selecting by collection
scene = bpy.context.scene
if self.entity == "Outdoor":
for obj in get_wmo_collection(scene, SpecialCollections.Outdoor).objects:
obj.select_set(True)
elif self.entity == "Indoor":
for obj in get_wmo_collection(scene, SpecialCollections.Indoor).objects:
obj.select_set(True)
elif self.entity == "wow_wmo_portal":
for obj in get_wmo_collection(scene, SpecialCollections.Portals).objects:
obj.select_set(True)
elif self.entity == "wow_wmo_liquid":
for obj in get_wmo_collection(scene, SpecialCollections.Liquids).objects:
obj.select_set(True)
elif self.entity == "wow_wmo_fog":
for obj in get_wmo_collection(scene, SpecialCollections.Fogs).objects:
obj.select_set(True)
elif self.entity == "wow_wmo_light":
for obj in get_wmo_collection(scene, SpecialCollections.Lights).objects:
obj.select_set(True)
elif self.entity == "wow_wmo_doodad":
for obj in get_wmo_collection(scene, SpecialCollections.Doodads).all_objects:
if not obj.hide_get():
obj.select_set(True)
elif self.entity == "Collision":
for obj in get_wmo_collection(scene, SpecialCollections.Collision).objects:
obj.select_set(True)
return {'FINISHED'}
class WMO_OT_generate_minimaps(bpy.types.Operator):
bl_idname = 'scene.wow_wmo_generate_minimaps'
bl_label = 'Generate Minimaps'
bl_description = 'Generate a wow minimap for WMO indoor groups(To the project folder)'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.scene.wow_scene.type == 'WMO'
def parse_existing_blp_strings(self, data):
existing_blp_strings = set()
lines = data.split(b'\r\n')
for line in lines:
if not line.startswith(b"dir:"):
try:
blp_string = line.split(b'\t')[-1].strip()
existing_blp_strings.add(blp_string.decode('utf-8'))
except Exception as e:
print(f"Error parsing line: {line} - {str(e)}")
return existing_blp_strings
def generate_random_blp_string(self, existing_blp_strings):
while True:
random_string = hashlib.md5(str(random.random()).encode()).hexdigest() + ".blp"
if random_string not in existing_blp_strings:
return random_string
def execute(self, context):
wow_model_collection = get_current_wow_model_collection(bpy.context.scene, 'wow_wmo')
if not wow_model_collection.wow_wmo.dir_path:
raise Exception("Game path is empty. You must set the model's client path in |Collection properties->Directory Path| and name your collection with your wmo name to use this feature.\n(Example : World\wmo\Dungeon\AZ_Deadmines for the path and AZ_Deadmines_A for the collection name")
wmo_path = str(wow_model_collection.wow_wmo.dir_path + '\\' + wow_model_collection.name)
md5_path = os.path.relpath(wmo_path.split('.')[0].lower(), 'world')
md5_entries = []
game_data = load_game_data()
try:
file, _ = game_data.read_file("textures\\Minimap\\md5translate.trs")
md5_file = io.BytesIO(file)
except KeyError:
raise FileNotFoundError("\nMD5 File <<{}>> not found in WoW file system.".format("textures\\Minimap\\md5translate.trs"))
md5_file = md5_file.read()
existing_strings = self.parse_existing_blp_strings(file)
###########
# TODOs:
# BLP conversion?
def create_camera_object():
# Return if a camera exists.
if bpy.data.cameras.find('MinimapsCamera') == -1:
bpy.data.cameras.new(name='MinimapsCamera')
if bpy.data.objects.find('MinimapsCamera') != -1:
if bpy.data.scenes["Scene"].collection.objects.find('MinimapsCamera') == -1:
bpy.data.scenes["Scene"].collection.objects.link(bpy.data.objects['MinimapsCamera'])
else:
cam_obj = bpy.data.objects.new('MinimapsCamera', bpy.data.cameras["MinimapsCamera"])
bpy.data.scenes["Scene"].collection.objects.link(cam_obj)
bpy.data.scenes["Scene"].camera = bpy.data.objects['MinimapsCamera']
def set_mat_backface_culling():
for group_object in get_wmo_groups_list(bpy.context.scene):
for material in group_object.data.materials:
material.use_backface_culling = True
def disable_object_wmo_render_visiblity():
for group_object in get_wmo_groups_list(bpy.context.scene):
group_object.hide_render = True
for obj in bpy.context.scene.objects:
obj.hide_render = True
def apply_render_settings():
bpy.context.scene.view_settings.view_transform = 'Filmic'
bpy.context.scene.view_settings.exposure = -0.5
bpy.context.scene.view_settings.gamma = 1.5
bpy.context.scene.view_settings.look = 'None'
bpy.context.scene.render.image_settings.file_format = 'PNG'
bpy.context.scene.render.image_settings.color_mode = 'RGBA'
bpy.context.scene.render.film_transparent = True
bpy.data.cameras["MinimapsCamera"].type = 'ORTHO'
bpy.data.cameras["MinimapsCamera"].ortho_scale = 128.0
def iterate_groups():
sorted_objects = sorted(get_wmo_groups_list(bpy.context.scene), key=lambda obj: obj.wow_wmo_group.export_order)
for i, wmo_group in tqdm(enumerate(sorted_objects)):
if WoWWMOGroup.is_indoor(wmo_group) and wmo_group.name != 'antiportal':
group_id = wmo_group.wow_wmo_group.export_order
render_images(wmo_group, group_id)
def render_images(obj, group_id):
bpy.context.view_layer.objects.active = obj
camera = bpy.data.cameras["MinimapsCamera"]
if not get_project_preferences().project_dir_path:
output_path = bpy.context.scene.render.filepath
else:
output_path = os.path.join(get_project_preferences().project_dir_path, r'textures\Minimap')
# md5_text = ""
md5_text = b''
def set_render_resolution(res):
if res == 128:
camera.ortho_scale = 64
elif res == 64:
camera.ortho_scale = 32
elif res == 32:
camera.ortho_scale = 16
else:
camera.ortho_scale = 128
bpy.context.scene.render.resolution_x = res
bpy.context.scene.render.resolution_y = res
def position_camera(bounds, offset_x, offset_y):
center_offset = camera.ortho_scale / 2
tile_offset_size = camera.ortho_scale
tile_x = tile_offset_size * offset_x
tile_y = tile_offset_size * offset_y
cam_position = [
bounds[0] + center_offset + tile_x,
bounds[1] + center_offset + tile_y,
bounds[2]
]
bpy.data.objects["MinimapsCamera"].location = cam_position
def add_md5_entry(offset_x, offset_y, md5_text):
offset_name = str(offset_x).zfill(2) + '_' + str(offset_y).zfill(2)
md5_a = md5_path + "_" + str(group_id).zfill(3) + '_' + offset_name + '.blp'
md5_b = self.png_name
md5_text += md5_a.encode() + b'\t' + md5_b.encode() + b'\r\n'
md5_entries.append(md5_text)
def renderliquid(liquidobj):
# create bmesh
# bm = bmesh.new()
# bm.from_object(liquidobj, bpy.context.evaluated_depsgraph_get())
bm = liquidobj.copy()
bpy.context.collection.objects.link(bm)
bpy.context.view_layer.update()
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.context.view_layer.objects.active = bm
bpy.ops.object.mode_set(mode = 'EDIT')
mesh = bm.data
renderflag_layer = mesh.vertex_colors['flag_0']
def comp_colors(color1, color2):
for i in range(3):
if color1[i] != color2[i]:
return False
return True
blue = [0.0, 0.0, 1.0]
for poly in mesh.polygons:
if comp_colors(renderflag_layer.data[poly.loop_indices[0]].color, blue):
poly.select = True
# bpy.ops.object.mode_set(mode = 'EDIT')
# bpy.ops.object.editmode_toggle()
bpy.ops.mesh.delete(type='FACE')
bpy.ops.object.mode_set(mode = 'OBJECT')
bm.hide_render = False
return bm
def render(offset_x, offset_y):
self.png_name = self.generate_random_blp_string(existing_strings)
png_name = self.png_name.replace('.blp', '')
bpy.context.scene.render.filepath = output_path + '\\' + png_name
obj.hide_render = False
# titi liquids
#liquidobj = obj.wow_wmo_group.liquid_mesh
#if liquidobj:
#bm = renderliquid(liquidobj)
bpy.ops.render.render(write_still=True)
obj.hide_render = True
#if liquidobj:
# bm.free()
#bm.hide_render = True
#bpy.ops.object.delete() # should delete previosuly selected liquid copy
bpy.context.scene.render.filepath = output_path
# titi, attempt to covnert to blp using png2blp
# minimaps format : DXTC, alphachannem 0 bit, header 1024, 1 mipmap
# img = PNG2BLP().load(pngData, uint32_t pngSize)
# blp = PNG2BLP().createBlpDxtInMemory(bool generateMipMaps, int dxtFormat, uint32_t& fileSize)
# with open(output_path + '\\' + png_name, "rb") as f:
#
# print("test blp")
# # img = PNG2BLP().load(f, 256)
# pngbytes = f.read()
# #
# # print(img) World\wmo\Dungeon\AZ_Deadmines\AZ_Deadmines_A.wmo
#
# # blpdata = PNG2BLP(pngbytes, 256).createBlpDxtInMemory(True, 1, 256)
# blpdata = PNG2BLP(pngbytes, len(pngbytes)).create_blp_paletted_in_memory(True, 1)
#
# print(blpdata)
#
# with open(output_path + '\\' + name + "_" + str(group_id).zfill(3) + '_' + offset_name + '.blp', "wb") as blp:
# blp.write(blpdata)
# write blp file
# Get necessary bounding box values
bounds = [v[:] for v in obj.bound_box]
bounds_size_x = abs(bounds[0][0] - bounds[4][0])
bounds_size_y = abs(bounds[0][1] - bounds[3][1])
if bounds_size_x <= 16 and bounds_size_y <= 16:
set_render_resolution(32)
elif bounds_size_x <= 32 and bounds_size_y <= 32:
set_render_resolution(64)
elif bounds_size_x <= 64 and bounds_size_y <= 64:
set_render_resolution(128)
else:
set_render_resolution(256)
tiles_x = int(math.ceil(bounds_size_x / 128))
tiles_y = int(math.ceil(bounds_size_y / 128))
for offset_x in range(tiles_x):
for offset_y in range(tiles_y):
position_camera(bounds[1], offset_x, offset_y)
render(offset_x, offset_y)
add_md5_entry(offset_x, offset_y, md5_text)
def write_md5_entries(md5_file):
md5_output = b''
md5_output += b'dir: ' + os.path.dirname(md5_path).encode() + b'\r\n'
for entry in md5_entries:
md5_output += entry
if not get_project_preferences().project_dir_path: # if project dir not set in settings, use blender's render path
output_path = os.path.join(bpy.context.scene.render.filepath, r'md5translate.trs')
else:
output_path = os.path.join(get_project_preferences().project_dir_path, r'textures\Minimap\md5translate.trs')
with open(output_path, "wb") as f:
f.write(md5_file)
f.write(md5_output)
# open the folder when done
os.startfile(os.path.dirname(output_path))
create_camera_object()
set_mat_backface_culling()
disable_object_wmo_render_visiblity()
apply_render_settings()
iterate_groups()
write_md5_entries(md5_file)
return {'FINISHED'}
@@ -0,0 +1,74 @@
import bpy
from ..enums import portal_dir_alg_enum
from ..custom_objects import WoWWMOPortal, WoWWMOGroup
class WMO_OT_bake_portal_relations(bpy.types.Operator):
bl_idname = 'scene.wow_bake_portal_relations'
bl_label = 'Bake portal relations'
bl_description = 'Bake portal relations'
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
def find_nearest_objects_pair(object, objects):
pairs = []
for obj in objects:
hit = obj.closest_point_on_mesh(
obj.matrix_world.inverted() @ (object.matrix_world @ object.data.polygons[0].center))
hit_dist = (obj.matrix_world @ hit[1] - object.matrix_world @ object.data.polygons[0].center).length
pairs.append((obj, hit_dist))
pairs.sort(key=lambda x: x[1])
return pairs[0][0], pairs[1][0]
if not bpy.context.selected_objects:
self.report({'ERROR'}, "No objects selected.")
return {'FINISHED'}
success = False
groups = tuple(x for x in bpy.context.scene.objects if WoWWMOGroup.match(x) and not x.hide_get())
for obj in bpy.context.selected_objects:
if WoWWMOPortal.match(obj):
direction = find_nearest_objects_pair(obj, groups)
obj.wow_wmo_portal.first = direction[0] if direction[0] else ""
obj.wow_wmo_portal.second = direction[1] if direction[1] else ""
success = True
if success:
self.report({'INFO'}, "Done baking portal relations.")
else:
self.report({'ERROR'}, "No portal objects found among selected objects.")
return {'FINISHED'}
class WMO_OT_invert_portals(bpy.types.Operator):
bl_idname = 'scene.wow_set_portal_dir_alg'
bl_label = 'Set portal direction algorithm'
bl_description = 'Set portal direction calculation algorithm.'
bl_options = {'REGISTER', 'UNDO'}
algorithm: bpy.props.EnumProperty(
items=portal_dir_alg_enum,
default="0"
)
def execute(self, context):
success = False
for ob in bpy.context.selected_objects:
if WoWWMOPortal.match(ob):
ob.wow_wmo_portal.algorithm = self.algorithm
success = True
if success:
self.report({'INFO'}, "Successfully inverted selected portals")
return {'FINISHED'}
else:
self.report({'ERROR'}, "No portals found among selected objects")
return {'CANCELLED'}
@@ -0,0 +1,112 @@
from ....utils.callbacks import on_release, string_property_validator, string_filter_internal_dir
from .common import panel_poll
from ..enums import root_flags_enum
import bpy
class WMO_PT_collection(bpy.types.Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'collection'
bl_label = 'World Map Object'
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.enabled = context.collection.wow_wmo.enabled
layout.prop(context.collection.wow_wmo, "dir_path")
col = layout.column()
col.separator()
col.prop(context.collection.wow_wmo, "flags")
col.separator()
if "2" in context.collection.wow_wmo.flags:
col.prop(context.collection.wow_wmo, "ambient_color")
col.separator()
col.prop(context.collection.wow_wmo, "skybox_path")
col.prop(context.collection.wow_wmo, "wmo_id")
def draw_header(self, context):
layout = self.layout
row = layout.row()
row.prop(context.collection.wow_wmo, 'enabled', text='')
@classmethod
def poll(cls, context):
return panel_poll(cls, context) and context.collection.name in context.scene.collection.children
@on_release()
def update_flags(self, context):
properties = bpy.data.node_groups.get('MO_Properties')
if properties:
properties.nodes['IsRenderPathUnified'].outputs[0].default_value = int('2' in self.flags)
properties.nodes['DoNotFixColorVertexAlpha'].outputs[0].default_value = int('1' in self.flags)
@on_release()
def update_ambient_color(self, context):
properties = bpy.data.node_groups.get('MO_Properties')
if properties:
properties.nodes['IntAmbientColor'].outputs[0].default_value = self.ambient_color
class WoWWMOCollectionPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name='Enabled'
, description='Enable this collection as a WMO object.'
, default=False
)
dir_path: bpy.props.StringProperty(
name='Directory path'
, description='Full path of the WMO in WoW filesystem.'
, options={'TEXTEDIT_UPDATE'}
, update=lambda self, ctx: string_property_validator(self, ctx
, name='dir_path'
, str_filter=string_filter_internal_dir
, lockable=True)
)
flags: bpy.props.EnumProperty(
name="Root flags",
description="WoW WMO root flags",
items=root_flags_enum,
options={"ENUM_FLAG"},
update=update_flags
)
ambient_color: bpy.props.FloatVectorProperty(
name="Ambient Color",
subtype='COLOR',
default=(1, 1, 1, 1),
size=4,
min=0.0,
max=1.0,
update=update_ambient_color
)
skybox_path: bpy.props.StringProperty(
name="Skybox Path",
description="Skybox for WMO (.MDX)",
default='',
)
wmo_id: bpy.props.IntProperty(
name="DBC ID",
description="Used in WMOAreaTable (optional)",
default=0,
)
def register():
bpy.types.Collection.wow_wmo = bpy.props.PointerProperty(type=WoWWMOCollectionPropertyGroup)
def unregister():
del bpy.types.Collection.wow_wmo
@@ -0,0 +1,15 @@
from ....ui.enums import WoWSceneTypes
import bpy
def panel_poll(cls, context: bpy.types.Context) -> bool:
"""
Common poll for some WMO panels to determined if a panel should be rendered..
:param cls: Panel.
:param context: Current context.
:return: True if should be rendered, else False.
"""
return (context.scene is not None
and context.scene.wow_scene.type == WoWSceneTypes.WMO.name)
@@ -0,0 +1,70 @@
from ....ui.locks import DepsgraphLock
from ....ui.panels import WBS_PT_object_properties_common
from ....ui.enums import WoWSceneTypes
from ..custom_objects import WoWWMODoodad
import bpy
class WMO_PT_doodad(WBS_PT_object_properties_common, bpy.types.Panel):
bl_label = "WMO Doodad"
bl_context = "object"
__wbs_custom_object_type__ = WoWWMODoodad
__wbs_scene_type__ = WoWSceneTypes.WMO
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(context.object.wow_wmo_doodad, "path")
layout.prop(context.object.wow_wmo_doodad, "color")
col = layout.column()
col.prop(context.object.wow_wmo_doodad, "flags")
def update_doodad_color(self, context):
mesh = self.id_data.data
# print(mesh) # <bpy_struct, Object("BOOTSLEATHERBROWN01.011") at 0x0000017A1A837208>
# print(type(mesh)) # <class 'bpy_types.Object'>
with DepsgraphLock():
for mat in mesh.materials: # TODO : this broke somehow
# mat.node_tree.nodes['DoodadColor'].outputs[0].default_value = self.color
mat.node_tree.nodes['DoodadColor'].attribute_type = 'OBJECT'
mat.node_tree.nodes['DoodadColor'].attribute_name = 'wow_wmo_doodad.color'
class WoWDoodadPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty()
""" Set by operators. To make an object a doodad. """
path: bpy.props.StringProperty(name="Path", description='Path of doodad in WoW filesystem.')
color: bpy.props.FloatVectorProperty(
name="Color",
subtype='COLOR',
size=4,
default=(1, 1, 1, 1),
min=0.0,
max=1.0,
update=update_doodad_color
)
flags: bpy.props.EnumProperty(
name="Settings",
description="WoW doodad instance settings",
items=[("1", "Accept Projected Tex.", ""),
("2", "Adjust lighting", ""),
("4", "Unknown", ""),
("8", "Unknown", "")],
options={"ENUM_FLAG"}
)
def register():
bpy.types.Object.wow_wmo_doodad = bpy.props.PointerProperty(type=WoWDoodadPropertyGroup)
def unregister():
bpy.types.Object.wow_wmo_doodad = None
@@ -0,0 +1,121 @@
from ....ui.panels import WBS_PT_object_properties_common
from ....ui.enums import WoWSceneTypes
from ..custom_objects import WoWWMOFog
import bpy
class WMO_PT_fog(WBS_PT_object_properties_common, bpy.types.Panel):
bl_label = "WMO Fog"
bl_context = "object"
__wbs_custom_object_type__ = WoWWMOFog
__wbs_scene_type__ = WoWSceneTypes.WMO
def draw(self, context):
layout = self.layout
layout.use_property_split = True
self.layout.prop(context.object.wow_wmo_fog, "ignore_radius")
self.layout.prop(context.object.wow_wmo_fog, "unknown")
self.layout.prop(context.object.wow_wmo_fog, "inner_radius")
self.layout.prop(context.object.wow_wmo_fog, "end_dist")
self.layout.prop(context.object.wow_wmo_fog, "start_factor")
self.layout.prop(context.object.wow_wmo_fog, "color1")
self.layout.prop(context.object.wow_wmo_fog, "end_dist2")
self.layout.prop(context.object.wow_wmo_fog, "start_factor2")
self.layout.prop(context.object.wow_wmo_fog, "color2")
def update_fog_color(self, context):
fog = self.id_data
fog.color = (self.color1[0], self.color1[1], self.color1[2], 0.5)
class WowFogPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty()
fog_id: bpy.props.IntProperty(
name="WMO Group ID",
description="Used internally for exporting",
default=0,
)
ignore_radius: bpy.props.BoolProperty(
name="Ignore Radius",
description="Ignore radius in CWorldView::QueryCameraFog",
default=False
)
unknown: bpy.props.BoolProperty(
name="Unknown Flag",
description="Check that in if you know what it is",
default=False
)
inner_radius: bpy.props.FloatProperty(
name="Inner Radius (%)",
description="A radius of fog starting to fade",
default=100.0,
min=0.0,
max=100.0
)
end_dist: bpy.props.FloatProperty(
name="Farclip",
description="Fog farclip",
default=70.0,
min=0.0,
max=2048.0
)
start_factor: bpy.props.FloatProperty(
name="Nearclip",
description="Fog nearclip",
default=0.1,
min=0.0,
max=1.0
)
color1: bpy.props.FloatVectorProperty(
name="Color",
subtype='COLOR',
default=(1, 1, 1),
min=0.0,
max=1.0,
update=update_fog_color
)
end_dist2: bpy.props.FloatProperty(
name="Underwater farclip",
description="Underwater fog farclip",
default=70.0,
min=0.0,
max=250.0
)
start_factor2: bpy.props.FloatProperty(
name="Underwater nearclip",
description="Underwater fog nearclip",
default=0.1,
min=0.0,
max=1.0
)
color2: bpy.props.FloatVectorProperty(
name="Underwater Color",
subtype='COLOR',
default=(1, 1, 1),
min=0.0,
max=1.0
)
def register():
bpy.types.Object.wow_wmo_fog = bpy.props.PointerProperty(type=WowFogPropertyGroup)
def unregister():
del bpy.types.Object.wow_wmo_fog
@@ -0,0 +1,172 @@
from .liquid import WMO_PT_liquid
from ..enums import *
from ..custom_objects import WoWWMOGroup
from ....ui.panels import WBS_PT_object_properties_common
from ....ui.enums import WoWSceneTypes
from ..custom_objects import *
from ..collections import get_wmo_collection, SpecialCollections
from collections import namedtuple
import bpy
class WMO_PT_wmo_group(WBS_PT_object_properties_common, bpy.types.Panel):
bl_label = "WMO Group"
bl_context = "object"
__wbs_custom_object_type__ = WoWWMOGroup
__wbs_scene_type__ = WoWSceneTypes.WMO
def draw(self, context):
self.layout.use_property_split = True
col = self.layout.column()
col.prop(context.object.wow_wmo_group, "export_order")
col.separator()
col.prop(context.object.wow_wmo_group, "description")
col.separator()
col.prop(context.object.wow_wmo_group, "flags")
col.separator()
box = col.box()
box.prop(context.object.wow_wmo_group, "fog1")
box.prop(context.object.wow_wmo_group, "fog2")
box.prop(context.object.wow_wmo_group, "fog3")
box.prop(context.object.wow_wmo_group, "fog4")
col.separator()
col.prop(context.object.wow_wmo_group, "group_dbc_id")
col.prop(context.object.wow_wmo_group, "liquid_type")
box = col.box()
box.prop(context.object.wow_wmo_group, "liquid_mesh")
if context.object.wow_wmo_group.liquid_mesh:
ctx_override = namedtuple('ctx_override', ('layout', 'object'))
ctx = ctx_override(box, context.object.wow_wmo_group.liquid_mesh)
WMO_PT_liquid.draw(ctx, ctx)
box.prop(context.object.wow_wmo_group, "collision_mesh")
def fog_validator(self, context):
scn = bpy.context.scene
if self.fog1 and (not WoWWMOFog.match(self.fog1) or self.fog1.name not in get_wmo_collection(scn, SpecialCollections.Fogs).objects):
self.fog1 = None
if self.fog2 and (not WoWWMOFog.match(self.fog2) or self.fog2.name not in get_wmo_collection(scn, SpecialCollections.Fogs).objects):
self.fog2 = None
if self.fog3 and (not WoWWMOFog.match(self.fog3) or self.fog3.name not in get_wmo_collection(scn, SpecialCollections.Fogs).objects):
self.fog3 = None
if self.fog4 and (not WoWWMOFog.match(self.fog4) or self.fog4.name not in get_wmo_collection(scn, SpecialCollections.Fogs).objects):
self.fog4 = None
def update_flags(self, context):
obj = context.object
if not obj:
obj = context.view_layer.objects.active
if not obj:
return
if '0' in self.flags:
obj.pass_index |= 0x20 # BlenderWMOObjectRenderFlags.HasVertexColor
mesh = obj.data
if 'Col' not in mesh.color_attributes:
vertex_color_layer = mesh.color_attributes.new(name="Col", type='BYTE_COLOR', domain='POINT')
else:
obj.pass_index &= ~0x20
if '1' in self.flags:
obj.pass_index |= 0x4 # BlenderWMOObjectRenderFlags.NoLocalLight
else:
obj.pass_index &= ~0x4
class WowWMOGroupPropertyGroup(bpy.types.PropertyGroup):
description: bpy.props.StringProperty(
name="Description",
description='Saved in the WMO file.'
)
export_order: bpy.props.IntProperty(
name="Export Order",
min=0,
max=999
)
flags: bpy.props.EnumProperty(
items=group_flag_enum,
options={'ENUM_FLAG'},
update=update_flags
)
group_dbc_id: bpy.props.IntProperty(
name="DBC Group ID",
description="WMO Group ID in DBC file"
)
liquid_type: bpy.props.EnumProperty(
items=liquid_type_enum,
name="LiquidType",
description="Fill this WMO group with selected liquid."
)
fog1: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Fog #1",
poll=lambda self, obj: WoWWMOFog.match(obj) and obj.name in get_wmo_collection(bpy.context.scene, SpecialCollections.Fogs).objects,
update=fog_validator
)
fog2: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Fog #2",
poll=lambda self, obj: WoWWMOFog.match(obj) and obj.name in get_wmo_collection(bpy.context.scene, SpecialCollections.Fogs).objects,
update=fog_validator
)
fog3: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Fog #3",
poll=lambda self, obj: WoWWMOFog.match(obj) and obj.name in get_wmo_collection(bpy.context.scene, SpecialCollections.Fogs).objects,
update=fog_validator
)
fog4: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Fog #4",
poll=lambda self, obj: WoWWMOFog.match(obj) and obj.name in get_wmo_collection(bpy.context.scene, SpecialCollections.Fogs).objects,
update=fog_validator
)
collision_mesh: bpy.props.PointerProperty(
type=bpy.types.Object,
name='Collision',
description='Invisible collision geometry of this group',
poll=lambda self, obj: obj.type == 'MESH' and obj.name in get_wmo_collection(bpy.context.scene, SpecialCollections.Collision).objects
)
liquid_mesh: bpy.props.PointerProperty(
type=bpy.types.Object,
name='Liquid',
description='Liquid plane linked to this group',
poll=lambda self, obj: obj.type == 'MESH' and WoWWMOLiquid.match(obj)
)
def register():
bpy.types.Object.wow_wmo_group = bpy.props.PointerProperty(type=WowWMOGroupPropertyGroup)
def unregister():
del bpy.types.Object.wow_wmo_group
@@ -0,0 +1,90 @@
import bpy
from ..enums import *
from ..custom_objects import WoWWMOLight
from ....ui.panels import WBS_PT_object_properties_common
from ....ui.enums import WoWSceneTypes
class WMO_PT_light(WBS_PT_object_properties_common, bpy.types.Panel):
bl_label = "WMO Light"
bl_context = "data"
__wbs_custom_object_type__ = WoWWMOLight
__wbs_scene_type__ = WoWSceneTypes.WMO
def draw(self, context):
self.layout.use_property_split = True
self.layout.prop(context.object.wow_wmo_light, "light_type")
self.layout.prop(context.object.wow_wmo_light, "use_attenuation")
self.layout.prop(context.object.wow_wmo_light, "color")
self.layout.prop(context.object.wow_wmo_light, "intensity")
self.layout.prop(context.object.wow_wmo_light, "attenuation_start")
self.layout.prop(context.object.wow_wmo_light, "attenuation_end")
class WowLightPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty()
light_type: bpy.props.EnumProperty(
items=light_type_enum,
name="Type",
description="Type of the lamp"
)
type: bpy.props.BoolProperty(
name="Type",
description="Unknown"
)
use_attenuation: bpy.props.BoolProperty(
name="Use attenuation",
description="True if lamp uses attenuation"
)
padding: bpy.props.BoolProperty(
name="Padding",
description="True if lamp uses padding"
)
color: bpy.props.FloatVectorProperty(
name="Color",
subtype='COLOR',
default=(1, 1, 1),
min=0.0,
max=1.0
)
intensity: bpy.props.FloatProperty(
name="Intensity",
description="Intensity of the lamp"
)
color_alpha: bpy.props.FloatProperty(
name="ColorAlpha",
description="Color alpha",
default=1,
min=0.0,
max=1.0
)
attenuation_start: bpy.props.FloatProperty(
name="Attenuation start",
description="Distance at which light intensity starts to decrease"
)
attenuation_end: bpy.props.FloatProperty(
name="Attenuation end",
description="Distance at which light intensity reach 0"
)
def register():
bpy.types.Object.wow_wmo_light = bpy.props.PointerProperty(type=WowLightPropertyGroup)
def unregister():
del bpy.types.Object.wow_wmo_light
@@ -0,0 +1,40 @@
from ..custom_objects import WoWWMOLiquid
from ....ui.panels import WBS_PT_object_properties_common
from ....ui.enums import WoWSceneTypes
import bpy
class WMO_PT_liquid(WBS_PT_object_properties_common, bpy.types.Panel):
bl_label = "WMO Liquid"
bl_context = "object"
__wbs_custom_object_type__ = WoWWMOLiquid
__wbs_scene_type__ = WoWSceneTypes.WMO
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(context.object.wow_wmo_liquid, "color")
class WowLiquidPropertyGroup(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty()
color: bpy.props.FloatVectorProperty(
name="Color",
subtype='COLOR',
default=(0.08, 0.08, 0.08, 1.0),
size=4,
min=0.0,
max=1.0
)
def register():
bpy.types.Object.wow_wmo_liquid = bpy.props.PointerProperty(type=WowLiquidPropertyGroup)
def unregister():
del bpy.types.Object.wow_wmo_liquid
@@ -0,0 +1,219 @@
import bpy
from ..enums import *
from ...bl_render import update_wmo_mat_node_tree
from ....utils.callbacks import on_release
from .common import panel_poll
from ...ui.custom_objects import WoWWMOGroup
from ....pywowlib import WoWVersions
class WMO_PT_material(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_label = "WMO Material"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
col = layout.column()
col.prop(context.material.wow_wmo_material, "shader")
col.prop(context.material.wow_wmo_material, "terrain_type")
col.prop(context.material.wow_wmo_material, "blending_mode")
col.separator()
box = col.box()
box.prop(context.material.wow_wmo_material, "diff_texture_1")
if context.material.wow_wmo_material.diff_texture_1:
box.prop(context.material.wow_wmo_material.diff_texture_1.wow_wmo_texture, "path")
# only display 2nd texture for multi tetxure shader types
if int(context.material.wow_wmo_material.shader) in (3, 5, 6, 7, 8, 9, 11, 12, 13, 15, 17):
box.prop(context.material.wow_wmo_material, "diff_texture_2")
if context.material.wow_wmo_material.diff_texture_2:
box.prop(context.material.wow_wmo_material.diff_texture_2.wow_wmo_texture, "path")
col.separator()
col.prop(context.material.wow_wmo_material, "flags")
layout.prop(context.material.wow_wmo_material, "emissive_color")
layout.prop(context.material.wow_wmo_material, "diff_color")
@classmethod
def poll(cls, context):
return (panel_poll(cls, context)
and context.material is not None
and WoWWMOGroup.match(context.object)
)
def update_flags(self, context):
material = self.id_data
if '1' in self.flags:
material.pass_index |= 0x1 # BlenderWMOMaterialRenderFlags.Unlit
else:
material.pass_index &= ~0x1
if '16' in self.flags:
material.pass_index |= 0x2 # BlenderWMOMaterialRenderFlags.SIDN
else:
material.pass_index &= ~0x2
def update_shader(self, context):
material = self.id_data
if int(self.shader) in (3, 5, 6, 7, 8, 9, 11, 12, 13, 15, 17):
material.pass_index |= 0x4 # BlenderWMOMaterialRenderFlags.IsTwoLayered
else:
material.pass_index &= ~0x4
def update_blending_mode(self, context):
material = self.id_data
blend_mode = int(self.blending_mode)
if blend_mode in (0, 8, 9):
material.pass_index |= 0x10 # BlenderWMOMaterialRenderFlags.IsOpaque
else:
material.pass_index &= ~0x10
if blend_mode in (0, 8, 9):
material.blend_method = 'OPAQUE'
elif blend_mode == 1:
material.blend_method = 'CLIP'
material.alpha_threshold = 0.9
# those blending modes don't exist anymore in 2.9+
# elif wmo_material.blend_mode in (3, 7, 10):
# mat.blend_method = 'ADD'
# elif wmo_material.blend_mode in (4, 5):
# mat.blend_method = 'MULTIPLY'
else:
material.blend_method = 'BLEND'
def update_diff_texture_1(self, context):
if not self.id_data.use_nodes or ('DiffuseTexture1' not in self.id_data.node_tree.nodes):
return
if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE') and self.diff_texture_1:
self.id_data.node_tree.nodes['DiffuseTexture1'].image = self.diff_texture_1
def update_diff_texture_2(self, context):
if not self.id_data.use_nodes or ('DiffuseTexture2' not in self.id_data.node_tree.nodes):
return
if bpy.context.scene.render.engine in ('CYCLES', 'BLENDER_EEVEE') and self.diff_texture_2:
self.id_data.node_tree.nodes['DiffuseTexture2'].image = self.diff_texture_2
@on_release()
def update_emissive_color(self, context):
if not self.id_data.use_nodes or ('EmissiveColor' not in self.id_data.node_tree.nodes):
return
self.id_data.node_tree.nodes['EmissiveColor'].outputs[0].default_value = self.emissive_color
def update_wmo_material_enabled(self, context):
if self.id_data:
update_wmo_mat_node_tree(self.id_data)
def set_shader_enum(self, context):
wow_version = int(bpy.context.scene.wow_scene.version)
if wow_version == WoWVersions.WOTLK:
tmp_shader_enum = [x for x in shader_enum if int(x[0]) < 7]
# elif wow_version == WoWVersions.LEGION:
# shader_enum = shader_enum
else:
tmp_shader_enum = shader_enum
return tmp_shader_enum
def set_terraintype_enum(self, context):
wow_version = int(bpy.context.scene.wow_scene.version)
if wow_version == WoWVersions.WOTLK:
tmp_terrain_type_enum = [x for x in terrain_type_enum if int(x[0]) < 12]
else:
tmp_terrain_type_enum = terrain_type_enum
return tmp_terrain_type_enum
class WowMaterialPropertyGroup(bpy.types.PropertyGroup):
flags: bpy.props.EnumProperty(
name="Material Flags",
description="WoW material flags",
items=material_flag_enum,
options={"ENUM_FLAG"},
update=update_flags
)
shader: bpy.props.EnumProperty(
items=set_shader_enum,
name="Shader",
description="WoW shader assigned to this material",
update=update_shader
)
blending_mode: bpy.props.EnumProperty(
items=blending_enum,
name="Blending",
description="WoW material blending mode",
update=update_blending_mode
)
emissive_color: bpy.props.FloatVectorProperty(
name="Emissive Color",
subtype='COLOR',
default=(1,1,1,1),
size=4,
min=0.0,
max=1.0,
update=update_emissive_color
)
diff_color: bpy.props.FloatVectorProperty(
name="Diffuse Color",
subtype='COLOR',
default=(1, 1, 1, 1),
size=4,
min=0.0,
max=1.0
)
terrain_type: bpy.props.EnumProperty(
items=set_terraintype_enum,
name="Terrain Type",
description="Terrain type assigned to this material. Used for producing correct footstep sounds."
)
diff_texture_1: bpy.props.PointerProperty(
type=bpy.types.Image,
name='Texture 1',
update=update_diff_texture_1
)
diff_texture_2: bpy.props.PointerProperty(
type=bpy.types.Image,
name='Texture 2',
update=update_diff_texture_2
)
def register():
bpy.types.Material.wow_wmo_material = bpy.props.PointerProperty(type=WowMaterialPropertyGroup)
def unregister():
del bpy.types.Material.wow_wmo_material
@@ -0,0 +1,107 @@
from ..enums import *
from ..custom_objects import WoWWMOPortal
from ....ui.panels import WBS_PT_object_properties_common
from ....ui.enums import WoWSceneTypes
from ....wmo.ui.custom_objects import WoWWMOGroup
from ....wmo.ui.collections import get_wmo_groups_list
from ...ui.enums import SpecialCollections
from ...ui.collections import get_wmo_collection
import bpy
class WMO_PT_portal(WBS_PT_object_properties_common, bpy.types.Panel):
bl_label = "WMO Portal"
bl_context = "object"
__wbs_custom_object_type__ = WoWWMOPortal
__wbs_scene_type__ = WoWSceneTypes.WMO
def draw(self, context):
layout = self.layout
layout.use_property_split = True
column = layout.column()
column.prop(context.object.wow_wmo_portal, "first")
column.prop(context.object.wow_wmo_portal, "second")
col = layout.column()
col.separator()
col.prop(context.object.wow_wmo_portal, "detail", expand=True)
col.separator()
col.prop(context.object.wow_wmo_portal, "algorithm", expand=True)
def portal_validator(self, context):
if self.second and not WoWWMOGroup.match(self.second):
self.second = None
if self.first and not WoWWMOGroup.match(self.first):
self.first = None
def build_first_group_list(self, context):
obj_list = []
# print(bpy.data.collections.get('Outdoor').objects)
scn = bpy.context.scene
for outdoor_obj in list(get_wmo_collection(scn, SpecialCollections.Outdoor).objects):
# print(outdoor_obj)
if self.second != outdoor_obj and outdoor_obj.name:
obj_list.append(outdoor_obj)
for indoor_obj in list(get_wmo_collection(scn, SpecialCollections.Indoor).objects):
print(indoor_obj)
if self.second != indoor_obj and indoor_obj.name:
obj_list.append(indoor_obj)
return obj_list
class WowPortalPlanePropertyGroup(bpy.types.PropertyGroup):
first: bpy.props.PointerProperty(
type=bpy.types.Object,
name="First group",
poll=lambda self, obj: WoWWMOGroup.match(obj) and self.second != obj and obj.name in bpy.context.scene.objects,
update=portal_validator
)
second: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Second group",
# doesn't work
# poll=lambda self, obj: WoWWMOGroup.match(obj) and self.first != obj and obj.name
# in get_wmo_groups_list(bpy.context.scene),
poll=lambda self, obj: WoWWMOGroup.match(obj) and self.first != obj and obj.name in bpy.context.scene.objects,
update=portal_validator
)
detail: bpy.props.EnumProperty(
items=portal_detail_enum,
name="Detail",
description="Disable this group will only work as a target for the portal. "
"See Stormwind cathedral for reference.",
default="0"
)
portal_id: bpy.props.IntProperty(
name="Portal's ID",
description="Portal ID"
)
algorithm: bpy.props.EnumProperty(
items=portal_dir_alg_enum,
name="Algorithm",
default="0"
)
def register():
bpy.types.Object.wow_wmo_portal = bpy.props.PointerProperty(type=WowPortalPlanePropertyGroup)
def unregister():
del bpy.types.Object.wow_wmo_portal
@@ -0,0 +1,36 @@
import bpy
from .common import panel_poll
class WMO_PT_texture(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "image"
bl_label = "WMO Texture"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
col = layout.column()
col.prop(context.edit_image.wow_wmo_texture, "path")
@classmethod
def poll(cls, context):
return panel_poll(cls, context) and context.image is not None
class WowWMOTexturePropertyGroup(bpy.types.PropertyGroup):
path: bpy.props.StringProperty(
name="Path",
description="Warning: texture path is applied on a per-texture (per-image), not on per-material basis."
)
def register():
bpy.types.Image.wow_wmo_texture = bpy.props.PointerProperty(type=WowWMOTexturePropertyGroup)
def unregister():
del bpy.types.Image.wow_wmo_texture
@@ -0,0 +1,335 @@
import bpy
from ....ui.preferences import get_project_preferences
from ..enums import *
from .common import panel_poll
from ..custom_objects import *
from ...ui.enums import SpecialCollections
from ...ui.collections import get_current_wow_model_collection, get_or_create_collection, get_wmo_groups_list
def update_wow_visibility(self, context):
values = self.wow_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":
if WoWWMOGroup.match(obj):
if WoWWMOGroup.is_outdoor(obj):
obj.hide_set('0' not in values)
elif WoWWMOGroup.is_indoor(obj):
obj.hide_set('1' not in values)
if obj.wow_wmo_group.collision_mesh:
col = obj.wow_wmo_group.collision_mesh
if 'wow_hide' not in col:
col['wow_hide'] = col.hide_get()
if col['wow_hide'] != col.hide_get():
continue
col.hide_set('6' not in values)
col['wow_hide'] = col.hide_get()
elif WoWWMOPortal.match(obj):
obj.hide_set('2' not in values)
elif WoWWMOFog.match(obj):
obj.hide_set('3' not in values)
elif WoWWMOLiquid.match(obj):
obj.hide_set('4' not in values)
elif obj.type == "LIGHT" and WoWWMOLight.match(obj):
obj.hide_set('5' not in values)
obj['wow_hide'] = obj.hide_get()
def update_wow_wmo_culling(self, context):
values = self.wow_enable_culling
for group_object in get_wmo_groups_list(bpy.context.scene):
for material in group_object.data.materials:
if not "4" in material.wow_wmo_material.flags: # two-sided flag
material.use_backface_culling = ('culling' in values)
def get_doodad_sets(self, context):
has_global = False
global_col = None
doodad_set_collections = set()
doodad_sets = []
wmo_model_collection = get_current_wow_model_collection(bpy.context.scene, 'wow_wmo')
if wmo_model_collection:
for set_collection in get_or_create_collection(wmo_model_collection, SpecialCollections.Doodads.name).children:
if "Set_$DefaultGlobal" not in set_collection.name:
doodad_set_collections.add(set_collection)
else:
has_global = True
global_col = set_collection
for index, set_collection in enumerate(sorted(doodad_set_collections, key=lambda x: x.name), 1 + has_global):
doodad_sets.append((set_collection.name, set_collection.name, "", 'SCENE_DATA', index))
doodad_sets.insert(0, ("None", "No set", "", 'X', 0))
if has_global:
doodad_sets.insert(1, (global_col.name, "Set_$DefaultGlobal", "", 'WORLD', 1))
return doodad_sets
def switch_doodad_set(self, context):
set = self.wow_doodad_visibility
wmo_model_collection = get_current_wow_model_collection(bpy.context.scene, 'wow_wmo')
if wmo_model_collection:
for set_collection in get_or_create_collection(wmo_model_collection, SpecialCollections.Doodads.name).children:
name = set_collection.name
for obj in set_collection.objects:
obj.hide_set(set == "None" or name != set and name != "Set_$DefaultGlobal")
class WMO_PT_tools_object_mode_display(bpy.types.Panel):
bl_label = 'Display'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_context = 'objectmode'
bl_category = 'WMO'
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, "wow_visibility")
col_col = col_row.column(align=True)
col_col.operator("scene.wow_wmo_select_entity", text='', icon='VIEWZOOM').entity = 'Outdoor'
col_col.operator("scene.wow_wmo_select_entity", text='', icon='VIEWZOOM').entity = 'Indoor'
col_col.operator("scene.wow_wmo_select_entity", text='', icon='VIEWZOOM').entity = 'wow_wmo_portal'
col_col.operator("scene.wow_wmo_select_entity", text='', icon='VIEWZOOM').entity = 'wow_wmo_fog'
col_col.operator("scene.wow_wmo_select_entity", text='', icon='VIEWZOOM').entity = 'wow_wmo_liquid'
col_col.operator("scene.wow_wmo_select_entity", text='', icon='VIEWZOOM').entity = 'wow_wmo_light'
col_col.operator("scene.wow_wmo_select_entity", text='', icon='VIEWZOOM').entity = 'Collision'
box2_row2 = col.row()
box2_row2.prop(context.scene, "wow_doodad_visibility", expand=False)
box2_row2.operator("scene.wow_wmo_select_entity", text='', icon='VIEWZOOM').entity = 'wow_wmo_doodad'
box2_row3 = col.row()
box2_row3.prop(context.scene, "wow_enable_culling")
@classmethod
def poll(cls, context):
return panel_poll(cls, context)
class WMO_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 = 'WMO'
def draw(self, context):
layout = self.layout.split()
wow_model_collection = get_current_wow_model_collection(bpy.context.scene, 'wow_wmo')
col = layout.column(align=True)
if wow_model_collection:
col.label(text=f'WMO: {wow_model_collection.name}')
col.separator()
if bpy.context.scene.wow_scene.doodadset_mode == False:
col.separator()
col1_col = col.column(align=True)
col1_row0 = col1_col.row(align=True)
col1_row1 = col1_col.row(align=True)
col1_row2 = col1_col.row(align=True)
col1_row3 = col1_col.row(align=True)
col1_row4 = col1_col.row(align=True)
col1_row4 = col1_col.row(align=True)
col1_row5 = col1_col.row(align=True)
col.separator()
if proj_prefs := get_project_preferences():
col1_row0.prop(proj_prefs, 'import_method', text='')
if proj_prefs.import_method == 'DirectPath':
box = col1_row1.box()
box.prop(proj_prefs, "direct_path")
col1_row2.operator("scene.wow_wmo_import_doodad_from_wmv", text='M2',
icon_value=ui_icons['WOW_STUDIO_DOODADS_ADD'])
col1_row2.operator("scene.wow_import_last_wmo_from_wmv", text='WMO',
icon_value=ui_icons['WOW_STUDIO_WMO_ADD'])
col1_row3.operator("scene.wow_add_fog", text='Fog', icon_value=ui_icons['WOW_STUDIO_FOG_ADD'])
col1_row3.operator("scene.wow_add_liquid", text='Liquid', icon_value=ui_icons['WOW_STUDIO_LIQUID_ADD'])
col1_row4.operator("scene.wow_add_light", text='Light', icon='LIGHT')
col1_row4.operator("scene.wow_add_scale_reference", text='Scale',
icon_value=ui_icons['WOW_STUDIO_SCALE_ADD'])
col1_row5.operator("scene.wow_wmo_texture_import", text='Texture', icon='IMAGE_DATA')
else:
col.separator()
col1_col = col.column(align=True)
col1_row0 = col1_col.row(align=True)
col1_row1 = col1_col.row(align=True)
col1_row2 = col1_col.row(align=True)
col1_row3 = col1_col.row(align=True)
col1_row4 = col1_col.row(align=True)
col.separator()
if proj_prefs := get_project_preferences():
col1_row0.prop(proj_prefs, 'import_method', text='')
col1_row2.operator("scene.wow_wmo_import_doodad_from_wmv", text='M2',
icon_value=ui_icons['WOW_STUDIO_DOODADS_ADD'])
@classmethod
def poll(cls, context):
return panel_poll(cls, context)
class WMO_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 = 'WMO'
def draw(self, context):
layout = self.layout.split()
col = layout.column(align=True)
col.separator()
box_col = col.column(align=True)
box_col.operator("scene.wow_wmo_generate_minimaps", text='Generate minimaps', icon='SHADING_RENDERED')
if bpy.context.selected_objects:
box_col.operator("scene.wow_wmo_generate_materials", text='Generate materials', icon='MATERIAL')
box_col.operator("scene.wow_fill_textures", text='Fill texture paths', icon='SEQ_SPLITVIEW')
box_col.operator("scene.wow_quick_collision", text='Quick collision', icon='MOD_TRIANGULATE')
box_col.operator("scene.wow_set_portal_dir_alg", text='Set portal direction', icon='ORIENTATION_NORMAL')
box_col.operator("scene.wow_bake_portal_relations", text='Bake portal relations', icon='FULLSCREEN_EXIT')
col.separator()
@classmethod
def poll(cls, context):
return context.scene is not None and context.scene.wow_scene.type == 'WMO' and bpy.context.scene.wow_scene.doodadset_mode == False
class WMO_PT_tools_object_mode_doodads(bpy.types.Panel):
bl_label = 'Doodads'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_context = 'objectmode'
bl_category = 'WMO'
@classmethod
def poll(cls, context):
return panel_poll(cls, context) and bpy.context.selected_objects
def draw(self, context):
layout = self.layout.split()
col = layout.column(align=True)
col.separator()
box_col2 = col.column(align=True)
box_col2.operator("scene.wow_doodads_bake_color", text='Bake color', icon='SHADING_RENDERED')
box_col2.operator("scene.wow_doodad_set_color", text='Set color', icon='SHADING_SOLID')
box_col2.operator("scene.wow_doodad_set_template_action", text='Template action', icon='STICKY_UVS_LOC')
col.separator()
class WMO_MT_mesh_wow_components_add(bpy.types.Menu):
bl_label = "WoW"
bl_options = {'REGISTER'}
def draw(self, context):
layout = self.layout
col = layout.column()
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 == 'WMO'
def wow_components_add_menu_item(self, context):
self.layout.menu("WMO_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 == 'WMO':
layout = self.layout
row = layout.row(align=True)
row.popover( panel="WMO_PT_tools_object_mode_display"
, text=''
, icon='HIDE_OFF'
)
def register():
bpy.types.Scene.wow_visibility = bpy.props.EnumProperty(
items=[
('0', "Outdoor", "Display outdoor groups", 'SELECT_SET', 0x1),
('1', "Indoor", "Display indoor groups", 'OBJECT_HIDDEN', 0x2),
('2', "Portals", "Display portals", 'FULLSCREEN_ENTER', 0x4),
('3', "Fogs", "Display fogs", 'MOD_FLUID', 0x8),
('4', "Liquids", "Display liquids", 'MOD_OCEAN', 0x10),
('5', "Lights", "Display lights", 'LIGHT', 0x20),
('6', "Collision", "Display collision", 'CON_SIZELIMIT', 0x40)],
options={'ENUM_FLAG'},
default={'0', '1', '2', '3', '4', '5'},
update=update_wow_visibility
)
bpy.types.Scene.wow_enable_culling = bpy.props.EnumProperty(
items=[
('culling', "Backface Culling",
"Enable materials backface culling unless that material has the two-sided flag for a realistic WoW rendering",
'XRAY', 0x1)],
options={'ENUM_FLAG'},
# default={},
update=update_wow_wmo_culling
)
bpy.types.Scene.wow_doodad_visibility = bpy.props.EnumProperty(
name="",
description="Switch doodad sets",
items=get_doodad_sets,
update=switch_doodad_set
)
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.wow_visibility
del bpy.types.Scene.wow_doodad_visibility
del bpy.types.Scene.wow_enable_culling
bpy.types.VIEW3D_MT_add.remove(wow_components_add_menu_item)
bpy.types.VIEW3D_MT_add.remove(render_viewport_toggles_right)
@@ -0,0 +1,47 @@
import bpy
from ..custom_objects import WoWWMOGroup
from .common import panel_poll
class WMO_PT_vertex_info(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "data"
bl_label = "WMO Vertex Info"
def draw(self, context):
self.layout.use_property_split = True
self.layout.prop_search(context.object.wow_wmo_vertex_info, "vertex_group",
context.object, "vertex_groups", text="Collision vertex group"
)
self.layout.prop(context.object.wow_wmo_vertex_info, "node_size", slider=True)
@classmethod
def poll(cls, context):
obj = context.object
return (panel_poll(cls, context)
and obj is not None
and WoWWMOGroup.match(obj)
)
class WowVertexInfoPropertyGroup(bpy.types.PropertyGroup):
vertex_group: bpy.props.StringProperty()
node_size: bpy.props.IntProperty(
name="Node max size",
description="Max count of faces for a node in bsp tree on export. 0 = Dynamic based on face count",
default=0, min=0,
soft_max=1000
)
def register():
bpy.types.Object.wow_wmo_vertex_info = bpy.props.PointerProperty(type=WowVertexInfoPropertyGroup)
def unregister():
del bpy.types.Object.wow_wmo_vertex_info