новая структура проекта
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
+211
@@ -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
|
||||
)
|
||||
+277
@@ -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)
|
||||
+28
@@ -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'
|
||||
+14
@@ -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')
|
||||
+331
@@ -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
|
||||
|
||||
|
||||
|
||||
+45
@@ -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)
|
||||
|
||||
+14
@@ -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')
|
||||
+381
@@ -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'}
|
||||
+32
@@ -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'}
|
||||
+56
@@ -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)
|
||||
+40
@@ -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'}
|
||||
+810
@@ -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'}
|
||||
+234
@@ -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'}
|
||||
+470
@@ -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'}
|
||||
+74
@@ -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'}
|
||||
+112
@@ -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
|
||||
+15
@@ -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)
|
||||
|
||||
+70
@@ -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
|
||||
+121
@@ -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
|
||||
+172
@@ -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
|
||||
+90
@@ -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
|
||||
|
||||
|
||||
+40
@@ -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
|
||||
+219
@@ -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
|
||||
+107
@@ -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
|
||||
|
||||
+36
@@ -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
|
||||
+335
@@ -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)
|
||||
+47
@@ -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
|
||||
Reference in New Issue
Block a user