новая структура проекта
This commit is contained in:
+82
@@ -0,0 +1,82 @@
|
||||
import os
|
||||
import bpy
|
||||
|
||||
from pathlib import Path
|
||||
from .cycles import update_wmo_mat_node_tree_cycles
|
||||
|
||||
|
||||
class BlenderWMOObjectRenderFlags:
|
||||
IsOutdoor = 0x1
|
||||
IsIndoor = 0x2
|
||||
NoLocalLight = 0x4
|
||||
HasBatchB = 0x8
|
||||
HasBatchA = 0x10
|
||||
HasVertexColor = 0x20
|
||||
HasBlendmap = 0x40
|
||||
HasLightmap = 0x80
|
||||
|
||||
class BlenderWMOMaterialRenderFlags:
|
||||
Unlit = 0x1
|
||||
SIDN = 0x2
|
||||
IsTwoLayered = 0x4
|
||||
IsOpaque = 0x10
|
||||
|
||||
node_groups = [
|
||||
'MO_ApplyLighting',
|
||||
'MO_ApplyLightingTrans',
|
||||
'MO_ApplyLightingGeneric',
|
||||
'MO_Equal',
|
||||
'MO_IsFlagInBitmask',
|
||||
'MO_IsInBatchMap',
|
||||
'MO_MaterialFlags',
|
||||
'MO_MixBatches',
|
||||
'MO_MixTextures',
|
||||
'MO_ObjectFlags',
|
||||
'MO_Properties',
|
||||
'MO_ApplyVertexColor',
|
||||
'MO_FixVertexColorAlpha',
|
||||
'MO_SaturateUpper',
|
||||
'MO_SetLighting',
|
||||
'MO_ShaderMix',
|
||||
'MO_WMOShader'
|
||||
]
|
||||
|
||||
|
||||
def load_wmo_shader_dependencies(reload_shader=False):
|
||||
render_engine = bpy.context.scene.render.engine
|
||||
|
||||
# remove old node groups
|
||||
if reload_shader:
|
||||
for ng_name in node_groups:
|
||||
if ng_name in bpy.data.node_groups:
|
||||
bpy.data.node_groups.remove(bpy.data.node_groups[ng_name])
|
||||
|
||||
missing_nodes = [ng_name for ng_name in node_groups if ng_name not in bpy.data.node_groups]
|
||||
|
||||
if render_engine in ('CYCLES', 'BLENDER_EEVEE'):
|
||||
lib_path = os.path.join(str(Path(__file__).parent), 'cycles', 'wotlk_default.blend')
|
||||
else:
|
||||
print('\nWARNING: Failed loading shader: materials may not display correctly.'
|
||||
'\nIncompatible render engine \""{}"\"'.format(render_engine))
|
||||
return
|
||||
|
||||
with bpy.data.libraries.load(lib_path) as (data_from, data_to):
|
||||
data_to.node_groups = [node_group for node_group in data_from.node_groups if node_group in missing_nodes]
|
||||
|
||||
|
||||
def update_wmo_mat_node_tree(bl_mat):
|
||||
|
||||
render_engine = bpy.context.scene.render.engine
|
||||
|
||||
if render_engine in ('CYCLES', 'BLENDER_EEVEE'):
|
||||
update_wmo_mat_node_tree_cycles(bl_mat)
|
||||
bpy.context.scene.wow_render_settings.sun_direction = bpy.context.scene.wow_render_settings.sun_direction
|
||||
|
||||
else:
|
||||
print('\nWARNING: Failed generating node tree: material \"{}\" may not display correctly.'
|
||||
'\nIncompatible render engine \""{}"\"'.format(bl_mat.name, render_engine))
|
||||
|
||||
# sync scene lighting properties
|
||||
bpy.context.scene.wow_render_settings.ext_ambient_color = bpy.context.scene.wow_render_settings.ext_ambient_color
|
||||
bpy.context.scene.wow_render_settings.ext_dir_color = bpy.context.scene.wow_render_settings.ext_dir_color
|
||||
bpy.context.scene.wow_render_settings.sidn_scalar = bpy.context.scene.wow_render_settings.sidn_scalar
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import bpy
|
||||
|
||||
from ....utils.node_builder import NodeTreeBuilder
|
||||
|
||||
|
||||
def update_wmo_mat_node_tree_cycles(bl_mat):
|
||||
|
||||
bl_mat.use_nodes = True
|
||||
tree = bl_mat.node_tree
|
||||
links = tree.links
|
||||
|
||||
tree_builder = NodeTreeBuilder(tree)
|
||||
|
||||
# get textures
|
||||
img_1 = bl_mat.wow_wmo_material.diff_texture_1 if bl_mat.wow_wmo_material.diff_texture_1 else None
|
||||
img_2 = bl_mat.wow_wmo_material.diff_texture_2 if bl_mat.wow_wmo_material.diff_texture_2 else None
|
||||
|
||||
# create nodes
|
||||
ng_wmo_shader = bpy.data.node_groups['MO_WMOShader']
|
||||
|
||||
blendmap = tree_builder.add_node('ShaderNodeAttribute', 'Blendmap', 0, 0)
|
||||
blendmap.attribute_name = 'Blendmap'
|
||||
|
||||
uvmap = tree_builder.add_node('ShaderNodeUVMap', 'UVMap', 0, 1)
|
||||
uvmap.uv_map = 'UVMap'
|
||||
|
||||
uvmap2 = tree_builder.add_node('ShaderNodeUVMap', 'UVMap', 0, 2)
|
||||
uvmap2.uv_map = 'UVMap.001'
|
||||
|
||||
emissive_color = tree_builder.add_node('ShaderNodeRGB', 'EmissiveColor', 1, 0)
|
||||
|
||||
diffuse_tex1 = tree_builder.add_node('ShaderNodeTexImage', 'DiffuseTexture1', 1, 1)
|
||||
if img_1:
|
||||
diffuse_tex1.image = img_1
|
||||
|
||||
diffuse_tex2 = tree_builder.add_node('ShaderNodeTexImage', 'DiffuseTexture2', 1, 2)
|
||||
if img_2:
|
||||
diffuse_tex2.image = img_2
|
||||
|
||||
shader = tree_builder.add_node('ShaderNodeGroup', 'WMO Shader', 2, 1)
|
||||
shader.node_tree = ng_wmo_shader
|
||||
|
||||
output = tree_builder.add_node('ShaderNodeOutputMaterial', 'Output', 3, 1)
|
||||
|
||||
# set node links
|
||||
links.new(blendmap.outputs['Color'], shader.inputs['Blendmap'])
|
||||
links.new(uvmap.outputs['UV'], diffuse_tex1.inputs['Vector'])
|
||||
links.new(uvmap2.outputs['UV'], diffuse_tex2.inputs['Vector'])
|
||||
links.new(emissive_color.outputs['Color'], shader.inputs['EmissiveColor'])
|
||||
links.new(diffuse_tex1.outputs['Color'], shader.inputs['DiffuseTexture1'])
|
||||
links.new(diffuse_tex1.outputs['Alpha'], shader.inputs['Alpha1'])
|
||||
links.new(diffuse_tex2.outputs['Color'], shader.inputs['DiffuseTexture2'])
|
||||
links.new(diffuse_tex2.outputs['Alpha'], shader.inputs['Alpha2'])
|
||||
links.new(shader.outputs['Shader'], output.inputs['Surface'])
|
||||
BIN
Binary file not shown.
+60
@@ -0,0 +1,60 @@
|
||||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://ddsywjux1ci4m"
|
||||
path="res://.godot/imported/wotlk_default.blend-8823381dcda7ad00ec8840b03f38568d.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://reference/blender-wow-studio-3.4-1.1.0_Experimental/io_scene_wmo/wmo/bl_render/cycles/wotlk_default.blend"
|
||||
dest_files=["res://.godot/imported/wotlk_default.blend-8823381dcda7ad00ec8840b03f38568d.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
blender/nodes/visible=0
|
||||
blender/nodes/active_collection_only=false
|
||||
blender/nodes/punctual_lights=true
|
||||
blender/nodes/cameras=true
|
||||
blender/nodes/custom_properties=true
|
||||
blender/nodes/modifiers=1
|
||||
blender/meshes/colors=false
|
||||
blender/meshes/uvs=true
|
||||
blender/meshes/normals=true
|
||||
blender/meshes/export_geometry_nodes_instances=false
|
||||
blender/meshes/gpu_instances=false
|
||||
blender/meshes/tangents=true
|
||||
blender/meshes/skins=2
|
||||
blender/meshes/export_bones_deforming_mesh_only=false
|
||||
blender/materials/unpack_enabled=true
|
||||
blender/materials/export_materials=1
|
||||
blender/animation/limit_playback=true
|
||||
blender/animation/always_sample=true
|
||||
blender/animation/group_tracks=true
|
||||
gltf/naming_version=2
|
||||
@@ -0,0 +1,48 @@
|
||||
from ..pywowlib import WoWVersionManager
|
||||
from ..pywowlib.wmo_file import WMOFile
|
||||
from ..third_party.tqdm import tqdm
|
||||
from .wmo_scene import BlenderWMOScene
|
||||
from ..ui.preferences import get_project_preferences
|
||||
|
||||
import bpy
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def export_wmo_from_blender_scene(filepath, client_version, export_selected, export_method):
|
||||
""" Export WoW WMO object from Blender scene to files """
|
||||
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
except:
|
||||
pass
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
WoWVersionManager().set_client_version(client_version)
|
||||
|
||||
wmo = WMOFile(client_version, filepath)
|
||||
wmo.export = export_method != 'PARTIAL'
|
||||
bl_scene = BlenderWMOScene(wmo, get_project_preferences())
|
||||
|
||||
bl_scene.build_references(export_selected, export_method)
|
||||
|
||||
bl_scene.save_materials()
|
||||
bl_scene.save_doodad_sets()
|
||||
bl_scene.save_lights()
|
||||
bl_scene.save_fogs()
|
||||
bl_scene.prepare_groups()
|
||||
bl_scene.save_portals()
|
||||
bl_scene.save_groups()
|
||||
bl_scene.save_root_header()
|
||||
|
||||
# create directory if it doesn't exist, for the new quick save
|
||||
file = Path(filepath)
|
||||
file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for _ in tqdm(range(1), desc='Writing WMO files', ascii=True):
|
||||
wmo.write()
|
||||
|
||||
|
||||
print("\nExport finished successfully. Saved WMO to " + filepath +
|
||||
"\nTotal export time: ", time.strftime("%M minutes %S seconds\a", time.gmtime(time.time() - start_time)))
|
||||
@@ -0,0 +1,107 @@
|
||||
import bpy
|
||||
import time
|
||||
import os
|
||||
import struct
|
||||
import timeit
|
||||
|
||||
from ..utils.misc import load_game_data
|
||||
from ..utils.collections import get_current_wow_model_collection, create_wmo_model_collection, SpecialCollection
|
||||
from .wmo_scene import BlenderWMOScene
|
||||
|
||||
from ..pywowlib import WoWVersionManager
|
||||
from ..pywowlib.wmo_file import WMOFile
|
||||
|
||||
from ..ui.preferences import get_project_preferences
|
||||
from .ui.handlers import DepsgraphLock
|
||||
from .ui.collections import WMO_SPECIAL_COLLECTION_TYPES, DoodadSetsCollection
|
||||
|
||||
|
||||
def import_wmo_to_blender_scene(filepath: str, client_version: int, wowfilepath: str = ''):
|
||||
""" Read and import WoW WMO object to Blender scene"""
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
WoWVersionManager().set_client_version(client_version)
|
||||
|
||||
print("\nImporting WMO")
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
game_data = load_game_data()
|
||||
|
||||
if not bpy.wow_game_data.files:
|
||||
raise Exception("WoW game data is not loaded. Check settings.")
|
||||
|
||||
if not project_preferences.cache_dir_path:
|
||||
raise Exception("Cache directory is not set, textures might not work. Check settings.")
|
||||
|
||||
with DepsgraphLock():
|
||||
wmo = WMOFile(client_version, filepath=filepath)
|
||||
# wmo.read()
|
||||
execution_time = timeit.timeit(lambda: wmo.read(), number=1)
|
||||
ms_time = execution_time * 1000
|
||||
print(f"Pywowlib WMO read time : {ms_time:.4f} ms")
|
||||
|
||||
wmo_scene = BlenderWMOScene(wmo=wmo, prefs=project_preferences)
|
||||
|
||||
# set wmo model collection
|
||||
wow_model_collection = get_current_wow_model_collection(bpy.context.scene, 'wow_wmo')
|
||||
if not wow_model_collection:
|
||||
wow_model_collection = create_wmo_model_collection(bpy.context.scene, filepath, wowfilepath)
|
||||
SpecialCollection.verify_root_collection_integrity(wow_model_collection, WMO_SPECIAL_COLLECTION_TYPES)
|
||||
DoodadSetsCollection.verify_doodad_sets_collection_integrity(bpy.context.scene, wow_model_collection)
|
||||
|
||||
# extract textures to cache folder
|
||||
game_data.extract_textures_as_png(project_preferences.cache_dir_path, wmo.motx.get_all_strings())
|
||||
|
||||
|
||||
# load all WMO components
|
||||
wmo_scene.load_materials()
|
||||
wmo_scene.load_lights()
|
||||
wmo_scene.load_properties()
|
||||
wmo_scene.load_fogs()
|
||||
wmo_scene.load_groups()
|
||||
wmo_scene.load_portals()
|
||||
wmo_scene.load_portal_relations()
|
||||
wmo_scene.load_doodads()
|
||||
|
||||
# update visibility
|
||||
bpy.context.scene.wow_visibility = bpy.context.scene.wow_visibility
|
||||
|
||||
print("\nDone importing WMO. \nTotal import time: ",
|
||||
time.strftime("%M minutes %S seconds.\a", time.gmtime(time.time() - start_time)))
|
||||
|
||||
|
||||
def import_wmo_to_blender_scene_gamedata(filepath: str, client_version: int):
|
||||
|
||||
filepath = filepath.replace('/', '\\')
|
||||
|
||||
game_data = load_game_data()
|
||||
|
||||
if not game_data or not game_data.files:
|
||||
raise FileNotFoundError("Game data is not loaded.")
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
cache_dir = project_preferences.cache_dir_path
|
||||
|
||||
game_data.extract_file(cache_dir, filepath)
|
||||
|
||||
if os.name != 'nt':
|
||||
filepath = filepath.lower()
|
||||
root_path = os.path.join(cache_dir, filepath.replace('\\', '/'))
|
||||
else:
|
||||
root_path = os.path.join(cache_dir, filepath)
|
||||
|
||||
with open(root_path, 'rb') as f:
|
||||
f.seek(24)
|
||||
n_groups = struct.unpack('I', f.read(4))[0]
|
||||
|
||||
group_paths = ["{}_{}.wmo".format(filepath[:-4], str(i).zfill(3)) for i in range(n_groups)]
|
||||
|
||||
game_data.extract_files(cache_dir, group_paths)
|
||||
|
||||
import_wmo_to_blender_scene(root_path, client_version, filepath)
|
||||
|
||||
# clean up unnecessary files and directories
|
||||
os.remove(root_path)
|
||||
for group_path in group_paths:
|
||||
os.remove(os.path.join(cache_dir, *group_path.split('\\')))
|
||||
@@ -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
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
import io
|
||||
import os
|
||||
import traceback
|
||||
|
||||
import bpy
|
||||
|
||||
from ...utils.misc import load_game_data
|
||||
from ..utils.materials import load_texture
|
||||
from ...utils.node_builder import NodeTreeBuilder
|
||||
from ...pywowlib.io_utils.types import *
|
||||
from ...ui.preferences import get_project_preferences
|
||||
|
||||
|
||||
# This file is implementing basic M2 geometry parsing in prodedural style for the sake of performance.
|
||||
class Submesh:
|
||||
__slots__ = (
|
||||
"start_vertex",
|
||||
"n_vertices",
|
||||
"start_triangle",
|
||||
"n_triangles",
|
||||
"texture_id",
|
||||
"blend_mode"
|
||||
)
|
||||
|
||||
def __init__(self, f):
|
||||
skip(f, 4)
|
||||
self.start_vertex = uint16.read(f)
|
||||
self.n_vertices = uint16.read(f)
|
||||
self.start_triangle = uint16.read(f)
|
||||
self.n_triangles = uint16.read(f)
|
||||
self.texture_id = None
|
||||
self.blend_mode = 0
|
||||
|
||||
skip(f, 36)
|
||||
|
||||
|
||||
def skip(f, n_bytes):
|
||||
f.seek(f.tell() + n_bytes)
|
||||
|
||||
|
||||
def import_doodad_model(asset_dir: str, filepath: str, placeholder: bool) -> bpy.types.Object:
|
||||
"""Import World of Warcraft M2 model to scene."""
|
||||
|
||||
game_data = load_game_data()
|
||||
|
||||
if placeholder:
|
||||
m2_path = os.path.splitext('Spells\\Errorcube.m2')[0] + ".m2"
|
||||
skin_path = os.path.splitext('Spells\\Errorcube.m2')[0] + "00.skin"
|
||||
real_m2_path = os.path.splitext(filepath)[0] + ".m2"
|
||||
m2_name = os.path.basename(os.path.splitext(real_m2_path)[0])
|
||||
else:
|
||||
m2_path = os.path.splitext(filepath)[0] + ".m2"
|
||||
skin_path = os.path.splitext(filepath)[0] + "00.skin"
|
||||
m2_name = os.path.basename(os.path.splitext(m2_path)[0])
|
||||
|
||||
try:
|
||||
file, _ = game_data.read_file(m2_path)
|
||||
m2_file = io.BytesIO(file)
|
||||
except KeyError:
|
||||
raise FileNotFoundError("\nModel <<{}>> not found in WoW file system.".format(filepath))
|
||||
|
||||
try:
|
||||
file, _ = game_data.read_file(skin_path)
|
||||
skin_file = io.BytesIO(file)
|
||||
except KeyError:
|
||||
raise FileNotFoundError("\nSkin file for model <<{}>> not found in WoW file system.".format(filepath))
|
||||
|
||||
###### M2 file parsing ######
|
||||
|
||||
f = m2_file
|
||||
magic = f.read(4).decode("utf-8")
|
||||
while magic not in ('MD20', 'MD21'):
|
||||
skip(f, uint32.read(f))
|
||||
magic = f.read(4).decode("utf-8")
|
||||
|
||||
# read flags
|
||||
f.seek(16)
|
||||
global_flags = uint32.read(f)
|
||||
|
||||
# read vertex positions
|
||||
f.seek(60)
|
||||
n_vertices = uint32.read(f)
|
||||
f.seek(uint32.read(f))
|
||||
|
||||
vertices = [(0.0, 0.0, 0.0)] * n_vertices
|
||||
normals = [(0.0, 0.0, 0.0)] * n_vertices
|
||||
uv_coords = [(0.0, 0.0)] * n_vertices
|
||||
|
||||
# if n_vertices == 0:
|
||||
# addon_dir = os.path.dirname(__file__)
|
||||
# placeholder_blend_path = os.path.join(addon_dir, "utils", "placeholders", "placeholder.blend")
|
||||
# directory = placeholder_blend_path + "\\Object\\"
|
||||
# bpy.ops.wm.append(filename='placeholder', directory=directory)
|
||||
# return import_doodad_model(asset_dir, filepath, True)
|
||||
|
||||
for i in range(n_vertices):
|
||||
vertices[i] = float32.read(f, 3)
|
||||
skip(f, 8)
|
||||
normals[i] = float32.read(f, 3)
|
||||
uv_coords[i] = float32.read(f, 2)
|
||||
skip(f, 8)
|
||||
|
||||
# read render flags and blending modes
|
||||
f.seek(112)
|
||||
n_render_flags = uint32.read(f)
|
||||
f.seek(uint32.read(f))
|
||||
|
||||
blend_modes = [] # skip render flags for now
|
||||
for _ in range(n_render_flags):
|
||||
skip(f, 2)
|
||||
blend_modes.append(uint16.read(f))
|
||||
|
||||
# read texture lookup
|
||||
f.seek(128)
|
||||
n_tex_lookups = uint32.read(f)
|
||||
f.seek(uint32.read(f))
|
||||
texture_lookup_table = [uint16.read(f) for _ in range(n_tex_lookups)]
|
||||
|
||||
# read texture names
|
||||
f.seek(80)
|
||||
n_textures = uint32.read(f)
|
||||
f.seek(uint32.read(f))
|
||||
|
||||
texture_paths = []
|
||||
for _ in range(n_textures):
|
||||
skip(f, 8)
|
||||
len_tex = uint32.read(f)
|
||||
ofs_tex = uint32.read(f)
|
||||
|
||||
if not len_tex:
|
||||
texture_paths.append(None)
|
||||
continue
|
||||
|
||||
pos = f.tell()
|
||||
f.seek(ofs_tex)
|
||||
texture_paths.append(f.read(len_tex).decode('utf-8').rstrip('\0'))
|
||||
f.seek(pos)
|
||||
|
||||
# read blend mode overrides
|
||||
if global_flags & 0x08:
|
||||
f.seek(304)
|
||||
n_blendmode_overrides = uint32.read(f)
|
||||
f.seek(uint32.read(f))
|
||||
|
||||
blend_mode_overrides = []
|
||||
for _ in range(n_blendmode_overrides):
|
||||
blend_mode_overrides.append(uint16.read(f))
|
||||
|
||||
###### Skin ######
|
||||
|
||||
f = skin_file
|
||||
padding = 0
|
||||
|
||||
# indices
|
||||
try:
|
||||
magic = f.read(4).decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
padding = 4
|
||||
f.seek(0)
|
||||
|
||||
n_indices = uint32.read(f)
|
||||
f.seek(uint32.read(f))
|
||||
indices = unpack('{}H'.format(n_indices), f.read(2 * n_indices))
|
||||
|
||||
# triangles
|
||||
f.seek(12 - padding)
|
||||
n_triangles = uint32.read(f) // 3
|
||||
f.seek(uint32.read(f))
|
||||
triangles = [(0, 0, 0)] * n_triangles
|
||||
for i in range(n_triangles):
|
||||
triangles[i] = unpack('HHH', f.read(6))
|
||||
|
||||
# submeshes
|
||||
f.seek(28 - padding)
|
||||
n_submeshes = uint32.read(f)
|
||||
f.seek(uint32.read(f))
|
||||
submeshes = [Submesh(f) for _ in range(n_submeshes)]
|
||||
|
||||
# texture units
|
||||
f.seek(36 - padding)
|
||||
n_tex_units = uint32.read(f)
|
||||
f.seek(uint32.read(f))
|
||||
for _ in range(n_tex_units):
|
||||
skip(f, 2)
|
||||
shader_id = uint16.read(f)
|
||||
submesh_id = uint16.read(f)
|
||||
|
||||
if submeshes[submesh_id].texture_id is not None:
|
||||
skip(f, 18)
|
||||
continue
|
||||
|
||||
skip(f, 4)
|
||||
render_flag_index = uint16.read(f)
|
||||
skip(f, 4)
|
||||
submeshes[submesh_id].texture_id = uint16.read(f)
|
||||
skip(f, 6)
|
||||
|
||||
submeshes[submesh_id].blend_mode = blend_modes[render_flag_index]
|
||||
|
||||
'''
|
||||
# determine blending mode
|
||||
if global_flags & 0x08 and shader_id < len(blend_mode_overrides):
|
||||
submeshes[submesh_id].blend_mode = blend_mode_overrides[shader_id]
|
||||
else:
|
||||
submeshes[submesh_id].blend_mode = blend_modes[render_flag_index]
|
||||
'''
|
||||
|
||||
###### Build blender object ######
|
||||
faces = [[0, 0, 0]] * n_triangles
|
||||
|
||||
for i, tri in enumerate(triangles):
|
||||
face = []
|
||||
for index in tri:
|
||||
face.append(indices[index])
|
||||
|
||||
faces[i] = face
|
||||
|
||||
# create mesh
|
||||
mesh = bpy.data.meshes.new(m2_name)
|
||||
mesh.from_pydata(vertices, [], faces)
|
||||
|
||||
for poly in mesh.polygons:
|
||||
poly.use_smooth = True
|
||||
|
||||
# set normals
|
||||
custom_normals = [(0.0, 0.0, 0.0)] * len(mesh.loops)
|
||||
mesh.use_auto_smooth = True
|
||||
for i, loop in enumerate(mesh.loops):
|
||||
custom_normals[i] = normals[loop.vertex_index]
|
||||
|
||||
mesh.normals_split_custom_set(custom_normals)
|
||||
|
||||
# set uv
|
||||
uv_layer1 = mesh.uv_layers.new(name="UVMap")
|
||||
for i in range(len(uv_layer1.data)):
|
||||
uv = uv_coords[mesh.loops[i].vertex_index]
|
||||
uv_layer1.data[i].uv = (uv[0], 1 - uv[1])
|
||||
|
||||
# unpack and convert textures
|
||||
game_data.extract_textures_as_png(asset_dir, texture_paths)
|
||||
|
||||
def import_placeholder(addon_relative_path, placeholder_object_name):
|
||||
current_dir = os.path.dirname(__file__)
|
||||
base_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
blend_path = os.path.join(base_dir, addon_relative_path)
|
||||
|
||||
directory = os.path.join(blend_path, "Object\\")
|
||||
|
||||
try:
|
||||
bpy.ops.wm.append(filename=placeholder_object_name, directory=directory)
|
||||
|
||||
#obj = bpy.ops.wm.append(filename=placeholder_object_name, directory=directory)
|
||||
if placeholder_object_name in bpy.data.objects:
|
||||
placeholder = bpy.data.objects.get(placeholder_object_name)
|
||||
nplaceholder = bpy.data.objects.new(m2_name, placeholder.data.copy())
|
||||
#placeholder_object_name.delete()
|
||||
#placeholder_duplicate = placeholder.copy()
|
||||
#placeholder_duplicate.data = placeholder.data.copy()
|
||||
bpy.data.objects.remove(placeholder, do_unlink=True)
|
||||
|
||||
return nplaceholder
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to import placeholder: {e}")
|
||||
return None
|
||||
|
||||
# create object
|
||||
if len(mesh.vertices) == 0 or placeholder:
|
||||
placeholder_object_name = 'Placeholder'
|
||||
addon_relative_path = 'utils\\placeholder\\placeholder.blend'
|
||||
nobj = import_placeholder(addon_relative_path, placeholder_object_name)
|
||||
nobj.wow_wmo_doodad.path = filepath
|
||||
nobj.wow_wmo_doodad.enabled = True
|
||||
image_name = "Placeholder"
|
||||
|
||||
img = bpy.data.images.get('Placeholder_Texture')
|
||||
if not img:
|
||||
img = bpy.data.images.new(name=image_name, width=512, height=512, alpha=True, float_buffer=False)
|
||||
img.generated_type = 'UV_GRID'
|
||||
#img = bpy.data.images['Placeholder_Texture']
|
||||
|
||||
mat = bpy.data.materials.get('Placeholder')
|
||||
if mat:
|
||||
nobj.data.materials.append(mat)
|
||||
|
||||
for block in bpy.data.meshes:
|
||||
if block.users == 0:
|
||||
bpy.data.meshes.remove(block)
|
||||
|
||||
for block in bpy.data.materials:
|
||||
if block.users == 0:
|
||||
bpy.data.materials.remove(block)
|
||||
|
||||
for block in bpy.data.textures:
|
||||
if block.users == 0:
|
||||
bpy.data.textures.remove(block)
|
||||
|
||||
for block in bpy.data.images:
|
||||
if block.users == 0:
|
||||
bpy.data.images.remove(block)
|
||||
|
||||
return nobj
|
||||
else:
|
||||
mat = bpy.data.materials.new(name="Placeholder")
|
||||
|
||||
mat.use_nodes = True
|
||||
|
||||
node_tree = mat.node_tree
|
||||
tree_builder = NodeTreeBuilder(node_tree)
|
||||
|
||||
tex_image = tree_builder.add_node('ShaderNodeTexImage', "Texture", 0, 0)
|
||||
tex_image.image = img
|
||||
|
||||
doodad_color= tree_builder.add_node('ShaderNodeAttribute', "DoodadColor", 0, 2)
|
||||
|
||||
mix_rgb = tree_builder.add_node('ShaderNodeMixRGB', "ApplyColor", 1, 1)
|
||||
mix_rgb.inputs['Fac'].default_value = 1.0
|
||||
mix_rgb.blend_type = 'MULTIPLY'
|
||||
|
||||
transparent_bsdf = tree_builder.add_node('ShaderNodeBsdfTransparent', "Transparent", 2, 0)
|
||||
bsdf = tree_builder.add_node('ShaderNodeBsdfDiffuse', "Diffuse", 2, 2)
|
||||
|
||||
mix_shader = tree_builder.add_node('ShaderNodeMixShader', "MixShader", 3, 0)
|
||||
mix_shader.inputs['Fac'].default_value = 1.0
|
||||
|
||||
output = tree_builder.add_node('ShaderNodeOutputMaterial', "Output", 4, 1)
|
||||
|
||||
mat.node_tree.links.new(tex_image.outputs['Color'], mix_rgb.inputs['Color1'])
|
||||
|
||||
mat.node_tree.links.new(doodad_color.outputs['Color'], mix_rgb.inputs['Color2'])
|
||||
mat.node_tree.links.new(mix_rgb.outputs['Color'], bsdf.inputs['Color'])
|
||||
mat.node_tree.links.new(bsdf.outputs['BSDF'], mix_shader.inputs[2])
|
||||
mat.node_tree.links.new(transparent_bsdf.outputs['BSDF'], mix_shader.inputs[1])
|
||||
mat.node_tree.links.new(mix_shader.outputs['Shader'], output.inputs['Surface'])
|
||||
mat.blend_method = 'OPAQUE'
|
||||
|
||||
nobj.data.materials.append(mat)
|
||||
|
||||
return nobj
|
||||
|
||||
else:
|
||||
nobj = bpy.data.objects.new(m2_name, mesh)
|
||||
|
||||
nobj.wow_wmo_doodad.path = filepath
|
||||
nobj.wow_wmo_doodad.enabled = True
|
||||
|
||||
# set textures
|
||||
texture_dir = get_project_preferences().cache_dir_path
|
||||
textures = {}
|
||||
for i, submesh in enumerate(submeshes):
|
||||
# tex_path = os.path.splitext(texture_paths[texture_lookup_table[submesh.texture_id]])[0] + '.png'
|
||||
tex_path = texture_paths[texture_lookup_table[submesh.texture_id]]
|
||||
|
||||
# add support for unix filesystems
|
||||
if os.name != 'nt':
|
||||
tex_path = tex_path.replace('\\', '/')
|
||||
|
||||
img = None
|
||||
|
||||
# try:
|
||||
# img = bpy.data.images.load(os.path.join(asset_dir, tex_path), check_existing=True)
|
||||
# except RuntimeError:
|
||||
# traceback.print_exc()
|
||||
# print("\nFailed to load texture: <<{}>>. File is missing or corrupted.".format(tex_path))
|
||||
|
||||
try:
|
||||
img = load_texture(textures, tex_path, texture_dir)
|
||||
except:
|
||||
print("\nFailed to load texture: <<{}>>. File is missing or corrupted.".format(tex_path))
|
||||
pass
|
||||
|
||||
if img:
|
||||
for j in range(submesh.start_triangle // 3, (submesh.start_triangle + submesh.n_triangles) // 3):
|
||||
mesh.polygons[j].material_index = i
|
||||
|
||||
mat = bpy.data.materials.new(name="{}_{}".format(m2_name, i))
|
||||
|
||||
mat.use_nodes = True
|
||||
|
||||
node_tree = mat.node_tree
|
||||
tree_builder = NodeTreeBuilder(node_tree)
|
||||
|
||||
tex_image = tree_builder.add_node('ShaderNodeTexImage', "Texture", 0, 0)
|
||||
tex_image.image = img
|
||||
|
||||
# doodad_color = tree_builder.add_node('ShaderNodeRGB', "DoodadColor", 0, 2)
|
||||
|
||||
doodad_color= tree_builder.add_node('ShaderNodeAttribute', "DoodadColor", 0, 2)
|
||||
|
||||
mix_rgb = tree_builder.add_node('ShaderNodeMixRGB', "ApplyColor", 1, 1)
|
||||
mix_rgb.inputs['Fac'].default_value = 1.0
|
||||
mix_rgb.blend_type = 'MULTIPLY'
|
||||
|
||||
transparent_bsdf = tree_builder.add_node('ShaderNodeBsdfTransparent', "Transparent", 2, 0)
|
||||
bsdf = tree_builder.add_node('ShaderNodeBsdfDiffuse', "Diffuse", 2, 2)
|
||||
|
||||
mix_shader = tree_builder.add_node('ShaderNodeMixShader', "MixShader", 3, 0)
|
||||
mix_shader.inputs['Fac'].default_value = 1.0
|
||||
|
||||
output = tree_builder.add_node('ShaderNodeOutputMaterial', "Output", 4, 1)
|
||||
|
||||
mat.node_tree.links.new(tex_image.outputs['Color'], mix_rgb.inputs['Color1'])
|
||||
|
||||
if submesh.blend_mode not in (0, 3):
|
||||
mat.node_tree.links.new(tex_image.outputs['Alpha'], mix_shader.inputs['Fac'])
|
||||
|
||||
mat.node_tree.links.new(doodad_color.outputs['Color'], mix_rgb.inputs['Color2'])
|
||||
mat.node_tree.links.new(mix_rgb.outputs['Color'], bsdf.inputs['Color'])
|
||||
mat.node_tree.links.new(bsdf.outputs['BSDF'], mix_shader.inputs[2])
|
||||
mat.node_tree.links.new(transparent_bsdf.outputs['BSDF'], mix_shader.inputs[1])
|
||||
mat.node_tree.links.new(mix_shader.outputs['Shader'], output.inputs['Surface'])
|
||||
|
||||
|
||||
# configure blending
|
||||
if submesh.blend_mode == 0:
|
||||
mat.blend_method = 'OPAQUE'
|
||||
elif submesh.blend_mode == 1:
|
||||
mat.blend_method = 'CLIP'
|
||||
mat.alpha_threshold = 0.9
|
||||
else:
|
||||
mat.blend_method = 'BLEND'
|
||||
|
||||
mesh.materials.append(mat)
|
||||
|
||||
return nobj
|
||||
|
||||
|
||||
def import_doodad(m2_path: str, cache_path: str) -> bpy.types.Object:
|
||||
|
||||
try:
|
||||
obj = import_doodad_model(cache_path, m2_path, False)
|
||||
except:
|
||||
obj = import_doodad_model(cache_path, m2_path, True)
|
||||
traceback.print_exc()
|
||||
print("\nFailed to import model: <<{}>>. Placeholder is imported instead.".format(m2_path))
|
||||
|
||||
obj.name = os.path.splitext(m2_path.split('\\')[-1])[0]
|
||||
|
||||
return obj
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import bpy
|
||||
|
||||
|
||||
def create_fog_object(name='WoW Fog', radius=1.0, location=None, color=(1.0, 1.0, 1.0, 1.0)):
|
||||
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(radius=radius
|
||||
, location=bpy.context.scene.cursor.location if location is None else location
|
||||
)
|
||||
|
||||
fog = bpy.context.view_layer.objects.active
|
||||
fog.name = name
|
||||
|
||||
# applying real object transformation
|
||||
bpy.ops.object.shade_smooth()
|
||||
|
||||
mesh = fog.data
|
||||
material = bpy.data.materials.new(name=name)
|
||||
mesh.materials.append(material)
|
||||
|
||||
material.use_nodes = True
|
||||
node_tree = material.node_tree
|
||||
|
||||
for node in node_tree.nodes:
|
||||
node_tree.nodes.remove(node)
|
||||
|
||||
output = material.node_tree.nodes.new('ShaderNodeOutputMaterial')
|
||||
diffuse = material.node_tree.nodes.new('ShaderNodeBsdfDiffuse')
|
||||
transparent = material.node_tree.nodes.new('ShaderNodeBsdfTransparent')
|
||||
mix = material.node_tree.nodes.new('ShaderNodeMixShader')
|
||||
color_node = material.node_tree.nodes.new('ShaderNodeAttribute')
|
||||
|
||||
node_tree.links.new(output.inputs['Surface'], mix.outputs['Shader'])
|
||||
node_tree.links.new(mix.inputs[1], transparent.outputs['BSDF'])
|
||||
node_tree.links.new(mix.inputs[2], diffuse.outputs['BSDF'])
|
||||
node_tree.links.new(diffuse.inputs['Color'], color_node.outputs['Color'])
|
||||
|
||||
mix.inputs['Fac'].default_value = 0.3
|
||||
# diffuse.inputs['Color'].default_value = color
|
||||
# color_node.inputs('Type').
|
||||
color_node.attribute_name = 'wow_wmo_fog.color1'
|
||||
color_node.attribute_type = 'OBJECT'
|
||||
|
||||
fog.hide_set(False if "3" in bpy.context.scene.wow_visibility else True)
|
||||
|
||||
bpy.context.view_layer.active_layer_collection.collection.objects.unlink(fog)
|
||||
|
||||
return fog
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import bpy
|
||||
import os
|
||||
from ...pywowlib.blp import BLP2PNG
|
||||
from typing import Dict
|
||||
|
||||
from ...utils.misc import load_game_data
|
||||
|
||||
# old: read from extracted cache images
|
||||
def load_texture_file(textures : dict, filepath : str, texture_dir : str) -> bpy.types.Image:
|
||||
new_filename = os.path.splitext(filepath)[0] + '.png'
|
||||
|
||||
if os.name != 'nt':
|
||||
new_filename = new_filename.replace('\\', '/')
|
||||
|
||||
texture = textures.get(filepath)
|
||||
|
||||
# if image is not loaded, do it
|
||||
if not texture:
|
||||
tex_img = bpy.data.images.load(os.path.join(texture_dir, new_filename))
|
||||
tex_img.wow_wmo_texture.path = filepath
|
||||
tex_img.name = os.path.basename(new_filename)
|
||||
texture = tex_img
|
||||
|
||||
textures[filepath] = texture
|
||||
|
||||
return texture
|
||||
|
||||
|
||||
def load_texture(textures : dict, filepath : str, texture_dir : str) -> bpy.types.Image:
|
||||
""" Load texture image from game data and add it to the blender scene """
|
||||
new_filename = os.path.splitext(filepath)[0] + '.png'
|
||||
|
||||
if os.name != 'nt':
|
||||
new_filename = new_filename.replace('\\', '/')
|
||||
|
||||
texture = textures.get(filepath)
|
||||
|
||||
if not texture:
|
||||
# try getting it from cache first ?
|
||||
save_path = os.path.join(texture_dir, new_filename)
|
||||
|
||||
# check file exists in the directory
|
||||
if os.path.isfile(save_path):
|
||||
# pretty much load_texture_file()
|
||||
tex_img = bpy.data.images.load(os.path.join(texture_dir, new_filename))
|
||||
texture = tex_img
|
||||
|
||||
else:
|
||||
# if not, load from game data and save it to directory.
|
||||
game_data = load_game_data()
|
||||
|
||||
result = game_data.read_file(filepath, "", 'blp', True)
|
||||
|
||||
if result is None:
|
||||
print("\nFailed to load texture: <<{}>> from gamedata.".format(filepath))
|
||||
return None
|
||||
|
||||
tex_img: bpy.types.Image = BLP2PNG().create_image(result[0])
|
||||
|
||||
# save it to the cache folder
|
||||
tex_img.save(filepath= save_path)
|
||||
tex_img.filepath = save_path
|
||||
|
||||
texture = tex_img
|
||||
|
||||
filepath = filepath.replace('/', '\\')
|
||||
texture.wow_wmo_texture.path = filepath
|
||||
texture.wow_m2_texture.path = filepath
|
||||
texture.name = os.path.basename(new_filename)
|
||||
|
||||
textures[filepath] = texture
|
||||
|
||||
return texture
|
||||
|
||||
|
||||
def add_ghost_material() -> bpy.types.Material:
|
||||
""" Add ghost material """
|
||||
|
||||
mat = bpy.data.materials.get("WowMaterial_ghost")
|
||||
if not mat:
|
||||
mat = bpy.data.materials.new("WowMaterial_ghost")
|
||||
mat.blend_method = 'BLEND'
|
||||
mat.use_nodes = True
|
||||
mat.node_tree.nodes.remove(mat.node_tree.nodes.get('Principled BSDF'))
|
||||
material_output = mat.node_tree.nodes.get('Material Output')
|
||||
transparent = mat.node_tree.nodes.new('ShaderNodeBsdfTransparent')
|
||||
mat.node_tree.links.new(material_output.inputs[0], transparent.outputs[0])
|
||||
mat.node_tree.nodes["Transparent BSDF"].inputs[0].default_value = (0.38, 0.89, 0.37, 1)
|
||||
|
||||
return mat
|
||||
@@ -0,0 +1,104 @@
|
||||
from typing import Union
|
||||
|
||||
from ...ui.preferences import get_project_preferences
|
||||
|
||||
|
||||
def wmv_get_last_wmo() -> Union[None, str]:
|
||||
"""Get the path of last WMO model from WoWModelViewer or similar log."""
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
if project_preferences.wmv_path:
|
||||
|
||||
lines = open(project_preferences.wmv_path).readlines()
|
||||
|
||||
for line in reversed(lines):
|
||||
if 'Loading WMO' in line:
|
||||
return line[22:].rstrip("\n")
|
||||
|
||||
|
||||
def wmv_get_last_m2() -> Union[None, str]:
|
||||
"""Get the path of last M2 model from WoWModelViewer or similar log."""
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
if project_preferences.wmv_path:
|
||||
|
||||
lines = open(project_preferences.wmv_path).readlines()
|
||||
|
||||
for line in reversed(lines):
|
||||
if 'Loading model:' in line:
|
||||
return line[25:].split(",", 1)[0].rstrip("\n")
|
||||
|
||||
|
||||
def wmv_get_last_texture() -> Union[None, str]:
|
||||
"""Get the path of last texture from WoWModelViewer or similar log."""
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
if project_preferences.wmv_path:
|
||||
|
||||
lines = open(project_preferences.wmv_path).readlines()
|
||||
|
||||
for line in reversed(lines):
|
||||
if 'Loading texture' in line:
|
||||
return line[27:].rstrip("\n")
|
||||
|
||||
def wow_export_get_last_texture() -> Union[None,str]:
|
||||
"""Get the path of last texture from WoWExport or similar log."""
|
||||
project_preferences = get_project_preferences()
|
||||
if project_preferences.wow_export_path:
|
||||
lines = open(project_preferences.wow_export_path).readlines()
|
||||
|
||||
for line in reversed(lines):
|
||||
if 'Previewing texture file' in line:
|
||||
return line[35:].split(",", 1)[0].rstrip("\n")
|
||||
|
||||
def wow_export_get_last_m2() -> Union[None, str]:
|
||||
"""Get the path of last M2 model from WoWExport or similar log."""
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
if project_preferences.wow_export_path:
|
||||
|
||||
lines = open(project_preferences.wow_export_path).readlines()
|
||||
|
||||
for line in reversed(lines):
|
||||
if 'Previewing model' in line:
|
||||
return line[28:].split(",", 1)[0].rstrip("\n")
|
||||
|
||||
def noggit_red_get_last_m2() -> Union[None, str]:
|
||||
"""Get the path of last M2 model from Noggit Red or similar log."""
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
if project_preferences.noggit_red_path:
|
||||
|
||||
lines = open(project_preferences.noggit_red_path).readlines()
|
||||
|
||||
for line in reversed(lines):
|
||||
if 'Loaded file' in line and 'm2' in line:
|
||||
start = line.find("'") + 1
|
||||
end = line.find(".m2") + 3
|
||||
return line[start:end]
|
||||
|
||||
def noggit_red_get_last_wmo() -> Union[None, str]:
|
||||
"""Get the path of last WMO model from Noggit Red or similar log."""
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
if project_preferences.noggit_red_path:
|
||||
|
||||
lines = open(project_preferences.noggit_red_path).readlines()
|
||||
|
||||
for line in reversed(lines):
|
||||
if 'Loaded file' in line and 'wmo' in line:
|
||||
start = line.find("'") + 1
|
||||
end = line.find(".wmo") + 4
|
||||
return line[start:end]
|
||||
|
||||
def wow_export_get_last_wmo() -> Union[None, str]:
|
||||
"""Get the path of last WMO model from WoWExport or similar log."""
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
if project_preferences.wow_export_path:
|
||||
|
||||
lines = open(project_preferences.wow_export_path).readlines()
|
||||
|
||||
for line in reversed(lines):
|
||||
if 'Previewing model' in line:
|
||||
return line[28:].split(",", 1)[0].rstrip("\n")
|
||||
@@ -0,0 +1,929 @@
|
||||
import hashlib
|
||||
import math
|
||||
import bpy
|
||||
import bmesh
|
||||
import typing
|
||||
|
||||
from mathutils import Vector, Matrix
|
||||
from bmesh.types import BMVert
|
||||
|
||||
from math import sqrt, atan2, pi
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
from .bl_render import update_wmo_mat_node_tree, load_wmo_shader_dependencies, BlenderWMOMaterialRenderFlags
|
||||
from .utils.fogs import create_fog_object
|
||||
from .utils.materials import add_ghost_material, load_texture
|
||||
from .utils.doodads import import_doodad
|
||||
from .wmo_scene_group import BlenderWMOSceneGroup
|
||||
from ..ui.preferences import get_project_preferences
|
||||
from ..utils.misc import find_nearest_object
|
||||
from ..wbs_kernel.wmo_utils import CWMOGeometryBatcher, WMOGeometryBatcherMeshParams
|
||||
from .ui.collections import get_wmo_collection, SpecialCollections, get_wmo_groups_list
|
||||
from ..utils.collections import get_current_wow_model_collection
|
||||
|
||||
from ..pywowlib.file_formats.wmo_format_root import GroupInfo, PortalInfo, PortalRelation, Fog
|
||||
from ..pywowlib.wmo_file import WMOFile
|
||||
from ..pywowlib import WoWVersions
|
||||
|
||||
from ..third_party.tqdm import tqdm
|
||||
|
||||
|
||||
class BlenderWMOScene:
|
||||
""" This class is used for assembling a Blender scene from a WNO file or saving the scene back to it."""
|
||||
|
||||
def __init__(self, wmo: WMOFile, prefs):
|
||||
self.wmo: WMOFile = wmo
|
||||
self.settings = prefs
|
||||
|
||||
self.bl_materials: Dict[int, bpy.types.Material] = {}
|
||||
self.bl_groups: List[BlenderWMOSceneGroup] = []
|
||||
self.bl_portals: List[bpy.types.Object] = []
|
||||
self.bl_fogs: List[bpy.types.Object] = []
|
||||
self.bl_lights: List[bpy.types.Object] = []
|
||||
self.bl_liquids: List[bpy.types.Object] = []
|
||||
self.bl_doodad_sets: Dict[str, bpy.types.Object] = {}
|
||||
# used for export:
|
||||
self.groups_eval: List[bpy.types.Mesh] = []
|
||||
self.group_batch_params: List[WMOGeometryBatcherMeshParams] = []
|
||||
self.portals_relations: Dict[bpy.types.Object, List[bpy.types.Object]] = {}
|
||||
self.lights_relations: Dict[bpy.types.Object, List[int]] = {}
|
||||
self.doodads_relations: Dict[bpy.types.Object, List[int]] = {}
|
||||
self.export_group_ids: Dict[bpy.types.Object, List[int]] = {}
|
||||
|
||||
def load_materials(self, texture_dir=None):
|
||||
""" Load materials from WoW WMO root file """
|
||||
|
||||
project_preferences = get_project_preferences()
|
||||
|
||||
if texture_dir is None:
|
||||
texture_dir = project_preferences.cache_dir_path
|
||||
|
||||
self.bl_materials = {0xFF : add_ghost_material()}
|
||||
|
||||
if 'MO_WMOShader' not in bpy.data.node_groups:
|
||||
load_wmo_shader_dependencies(reload_shader=True)
|
||||
|
||||
textures = {}
|
||||
|
||||
for index, wmo_material in tqdm(list(enumerate(self.wmo.momt.materials)), desc='Importing materials', ascii=True):
|
||||
texture1 = self.wmo.motx.get_string(wmo_material.texture1_ofs)
|
||||
texture2 = self.wmo.motx.get_string(wmo_material.texture2_ofs)
|
||||
|
||||
mat = bpy.data.materials.new(texture1.split('\\')[-1][:-4] + '.png')
|
||||
self.bl_materials[index] = mat
|
||||
|
||||
try:
|
||||
mat.wow_wmo_material.shader = str(wmo_material.shader)
|
||||
except TypeError:
|
||||
print("Incorrect shader id \"{}\". Most likely badly retro-ported WMO.".format(str(wmo_material.shader)))
|
||||
mat.wow_wmo_material.shader = "0"
|
||||
|
||||
mat.wow_wmo_material.blending_mode = str(wmo_material.blend_mode)
|
||||
mat.wow_wmo_material.emissive_color = [x / 255 for x in wmo_material.emissive_color]
|
||||
mat.wow_wmo_material.diff_color = (wmo_material.diff_color[2] / 255,
|
||||
wmo_material.diff_color[1] / 255,
|
||||
wmo_material.diff_color[0] / 255,
|
||||
wmo_material.diff_color[3] / 255
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
mat.wow_wmo_material.terrain_type = str(wmo_material.terrain_type)
|
||||
except TypeError as e:
|
||||
print('Terrain type not found for ', mat, e)
|
||||
mat.wow_wmo_material.terrain_type = '0'
|
||||
|
||||
mat_flags = set()
|
||||
bit = 1
|
||||
while bit <= 0x80:
|
||||
if wmo_material.flags & bit:
|
||||
mat_flags.add(str(bit))
|
||||
bit <<= 1
|
||||
mat.wow_wmo_material.flags = mat_flags
|
||||
|
||||
# create texture slots and load textures
|
||||
|
||||
if texture1:
|
||||
try:
|
||||
tex = load_texture(textures, texture1, texture_dir)
|
||||
mat.wow_wmo_material.diff_texture_1 = tex
|
||||
except:
|
||||
pass
|
||||
|
||||
if texture2:
|
||||
|
||||
try:
|
||||
tex = load_texture(textures, texture2, texture_dir)
|
||||
mat.wow_wmo_material.diff_texture_2 = tex
|
||||
except:
|
||||
pass
|
||||
|
||||
update_wmo_mat_node_tree(mat)
|
||||
|
||||
# set render flags
|
||||
pass_index = 0
|
||||
|
||||
if wmo_material.flags & 0x1:
|
||||
pass_index |= BlenderWMOMaterialRenderFlags.Unlit
|
||||
|
||||
if wmo_material.flags & 0x10:
|
||||
pass_index |= BlenderWMOMaterialRenderFlags.SIDN
|
||||
|
||||
if wmo_material.shader in (3, 5, 6, 7, 8, 9, 11, 12, 13, 15):
|
||||
pass_index |= BlenderWMOMaterialRenderFlags.IsTwoLayered
|
||||
|
||||
if wmo_material.blend_mode in (0, 8, 9):
|
||||
pass_index |= BlenderWMOMaterialRenderFlags.IsOpaque
|
||||
|
||||
# configure blending
|
||||
if wmo_material.blend_mode in (0, 8, 9):
|
||||
mat.blend_method = 'OPAQUE'
|
||||
elif wmo_material.blend_mode == 1:
|
||||
mat.blend_method = 'CLIP'
|
||||
mat.alpha_threshold = 0.9
|
||||
# TODO : 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:
|
||||
mat.blend_method = 'BLEND'
|
||||
|
||||
mat.pass_index = pass_index
|
||||
|
||||
|
||||
def load_lights(self):
|
||||
""" Load WoW WMO MOLT lights """
|
||||
|
||||
bl_light_types = ['POINT', 'SPOT', 'SUN', 'POINT']
|
||||
|
||||
scn = bpy.context.scene
|
||||
light_collection = get_wmo_collection(scn, SpecialCollections.Lights)
|
||||
|
||||
for i, wmo_light in tqdm(list(enumerate(self.wmo.molt.lights)), desc='Importing lights', ascii=True):
|
||||
|
||||
try:
|
||||
l_type = bl_light_types[wmo_light.light_type]
|
||||
except IndexError:
|
||||
raise Exception("Light type unknown : {} (light nbr : {})".format(str(wmo_light.LightType), str(i)))
|
||||
|
||||
light_name = "{}_Light_{}".format(self.wmo.display_name, str(i).zfill(2))
|
||||
|
||||
light = bpy.data.lights.new(light_name, l_type)
|
||||
obj = bpy.data.objects.new(light_name, light)
|
||||
obj.location = self.wmo.molt.lights[i].position
|
||||
|
||||
if wmo_light.type in {0, 2, 3}:
|
||||
light.type = 'POINT'
|
||||
elif wmo_light.type == 1:
|
||||
light.type = 'SPOT'
|
||||
|
||||
# TODO set rotation for spot lights. currently wmo_light.unknown1
|
||||
|
||||
|
||||
light.color = (wmo_light.color[2] / 255, wmo_light.color[1] / 255, wmo_light.color[0] / 255)
|
||||
light.energy = wmo_light.intensity
|
||||
|
||||
if wmo_light.light_type in {0, 1}:
|
||||
light.falloff_type = 'INVERSE_LINEAR'
|
||||
light.distance = wmo_light.unknown4 / 2
|
||||
|
||||
obj.wow_wmo_light.enabled = True
|
||||
obj.wow_wmo_light.light_type = str(wmo_light.light_type)
|
||||
obj.wow_wmo_light.type = bool(wmo_light.type)
|
||||
obj.wow_wmo_light.use_attenuation = bool(wmo_light.use_attenuation)
|
||||
obj.wow_wmo_light.padding = bool(wmo_light.padding)
|
||||
obj.wow_wmo_light.type = bool(wmo_light.type)
|
||||
obj.wow_wmo_light.color = light.color
|
||||
obj.wow_wmo_light.color_alpha = wmo_light.color[3] / 255
|
||||
obj.wow_wmo_light.intensity = wmo_light.intensity
|
||||
obj.wow_wmo_light.attenuation_start = wmo_light.attenuation_start
|
||||
obj.wow_wmo_light.attenuation_end = wmo_light.attenuation_end
|
||||
|
||||
self.bl_lights.append(light)
|
||||
|
||||
# move lights to collection
|
||||
light_collection.objects.link(obj)
|
||||
|
||||
def load_fogs(self):
|
||||
""" Load fogs from WMO Root File"""
|
||||
|
||||
fog_collection = get_wmo_collection(bpy.context.scene, SpecialCollections.Fogs)
|
||||
|
||||
for i, wmo_fog in tqdm(list(enumerate(self.wmo.mfog.fogs)), desc='Importing fogs', ascii=True):
|
||||
|
||||
fog_obj = create_fog_object( name="{}_Fog_{}".format(self.wmo.display_name, str(i).zfill(2))
|
||||
, location=wmo_fog.position
|
||||
#, radius=wmo_fog.big_radius
|
||||
, color=(wmo_fog.color1[2] / 255,
|
||||
wmo_fog.color1[1] / 255,
|
||||
wmo_fog.color1[0] / 255,
|
||||
0.0
|
||||
)
|
||||
)
|
||||
|
||||
fog_obj.scale = (wmo_fog.big_radius,wmo_fog.big_radius,wmo_fog.big_radius)
|
||||
|
||||
# applying object properties
|
||||
fog_obj.wow_wmo_fog.enabled = True
|
||||
fog_obj.wow_wmo_fog.ignore_radius = wmo_fog.flags & 0x01
|
||||
try:
|
||||
fog_obj.wow_wmo_fog.unknown = wmo_fog.flags & 0x10
|
||||
except:
|
||||
fog_obj.wow_wmo_fog.unknown = False
|
||||
|
||||
if wmo_fog.small_radius != 0:
|
||||
fog_obj.wow_wmo_fog.inner_radius = wmo_fog.small_radius / wmo_fog.big_radius * 100
|
||||
else:
|
||||
fog_obj.wow_wmo_fog.inner_radius = 0
|
||||
|
||||
fog_obj.wow_wmo_fog.end_dist = wmo_fog.end_dist
|
||||
fog_obj.wow_wmo_fog.start_factor = wmo_fog.start_factor
|
||||
fog_obj.wow_wmo_fog.color1 = (wmo_fog.color1[2] / 255, wmo_fog.color1[1] / 255, wmo_fog.color1[0] / 255)
|
||||
fog_obj.wow_wmo_fog.end_dist2 = wmo_fog.end_dist2
|
||||
fog_obj.wow_wmo_fog.start_factor2 = wmo_fog.start_factor2
|
||||
fog_obj.wow_wmo_fog.color2 = (wmo_fog.color2[2] / 255, wmo_fog.color2[1] / 255, wmo_fog.color2[0] / 255)
|
||||
|
||||
self.bl_fogs.append(fog_obj)
|
||||
|
||||
# move fogs to collection
|
||||
fog_collection.objects.link(fog_obj)
|
||||
|
||||
def load_doodads(self):
|
||||
|
||||
cache_path = self.settings.cache_dir_path
|
||||
doodad_prototypes = {}
|
||||
|
||||
scene = bpy.context.scene
|
||||
doodad_collection = get_wmo_collection(scene, SpecialCollections.Doodads)
|
||||
|
||||
with tqdm(self.wmo.modd.definitions, desc='Importing doodads', ascii=True) as progress:
|
||||
for doodad_set in self.wmo.mods.sets:
|
||||
# replace anchor object by collections
|
||||
doodadset_coll = bpy.data.collections.get(doodad_set.name)
|
||||
if not doodadset_coll:
|
||||
doodadset_coll = bpy.data.collections.new(doodad_set.name)
|
||||
doodad_collection.children.link(doodadset_coll)
|
||||
|
||||
doodadset_coll.color_tag = 'COLOR_04'
|
||||
|
||||
for i in range(doodad_set.start_doodad, doodad_set.start_doodad + doodad_set.n_doodads):
|
||||
doodad = self.wmo.modd.definitions[i]
|
||||
|
||||
doodad_path = self.wmo.modn.get_string(doodad.name_ofs)
|
||||
path_hash = str(hashlib.md5(doodad_path.encode('utf-8')).hexdigest())
|
||||
|
||||
proto_obj = doodad_prototypes.get(path_hash)
|
||||
|
||||
if not proto_obj:
|
||||
nobj = import_doodad(doodad_path, cache_path)
|
||||
doodad_prototypes[path_hash] = nobj
|
||||
else:
|
||||
nobj = proto_obj.copy()
|
||||
# nobj.data = nobj.data.copy()
|
||||
|
||||
# for j, mat in enumerate(nobj.data.materials):
|
||||
# nobj.data.materials[j] = mat.copy()
|
||||
|
||||
# also link to base collection ?
|
||||
doodadset_coll.objects.link(nobj)
|
||||
|
||||
bpy.context.view_layer.objects.active = nobj
|
||||
|
||||
nobj.wow_wmo_doodad.color = (doodad.color[2] / 255,
|
||||
doodad.color[1] / 255,
|
||||
doodad.color[0] / 255,
|
||||
doodad.color[3] / 255
|
||||
)
|
||||
|
||||
flags = []
|
||||
bit = 1
|
||||
while bit <= 0x8:
|
||||
if doodad.flags & bit:
|
||||
flags.append(str(bit))
|
||||
bit <<= 1
|
||||
|
||||
nobj.wow_wmo_doodad.flags = set(flags)
|
||||
|
||||
# place the object correctly on the scene
|
||||
nobj.location = doodad.position
|
||||
nobj.scale = (doodad.scale, doodad.scale, doodad.scale)
|
||||
|
||||
nobj.rotation_mode = 'QUATERNION'
|
||||
nobj.rotation_quaternion = (doodad.rotation[3],
|
||||
doodad.rotation[0],
|
||||
doodad.rotation[1],
|
||||
doodad.rotation[2])
|
||||
nobj.hide_set(True)
|
||||
|
||||
# doodad_collection.objects.link(nobj)
|
||||
|
||||
progress.update(1)
|
||||
|
||||
def load_portals(self):
|
||||
""" Load WoW WMO portal planes """
|
||||
portal_collection = get_wmo_collection(bpy.context.scene, SpecialCollections.Portals)
|
||||
|
||||
vert_count = 0
|
||||
for index, portal in tqdm(list(enumerate(self.wmo.mopt.infos)), desc='Importing portals', ascii=True):
|
||||
portal_name = "{}_Portal_{}".format(self.wmo.display_name, str(index).zfill(3))
|
||||
|
||||
verts = []
|
||||
face = []
|
||||
faces = []
|
||||
|
||||
for j in range(portal.n_vertices):
|
||||
if len(face) < 4:
|
||||
verts.append(self.wmo.mopv.portal_vertices[vert_count])
|
||||
face.append(j)
|
||||
vert_count += 1
|
||||
|
||||
faces.append(face)
|
||||
|
||||
mesh = bpy.data.meshes.new(portal_name)
|
||||
|
||||
obj = bpy.data.objects.new(portal_name, mesh)
|
||||
|
||||
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
|
||||
self.bl_portals.append(obj)
|
||||
|
||||
# assign portal material
|
||||
portal_mat = bpy.data.materials.get("WowMaterial_ghost_Portal")
|
||||
if portal_mat is None:
|
||||
portal_mat = bpy.data.materials.new("WowMaterial_ghost_Portal")
|
||||
portal_mat.blend_method = 'BLEND'
|
||||
portal_mat.use_nodes = True
|
||||
portal_mat.node_tree.nodes.remove(portal_mat.node_tree.nodes.get('Principled BSDF'))
|
||||
material_output = portal_mat.node_tree.nodes.get('Material Output')
|
||||
transparent = portal_mat.node_tree.nodes.new('ShaderNodeBsdfTransparent')
|
||||
portal_mat.node_tree.links.new(material_output.inputs[0], transparent.outputs[0])
|
||||
portal_mat.node_tree.nodes["Transparent BSDF"].inputs[0].default_value = (1, 0, 0, 1)
|
||||
|
||||
obj.data.materials.append(portal_mat)
|
||||
|
||||
# move portals to collection
|
||||
portal_collection.objects.link(obj)
|
||||
|
||||
def load_portal_relations(self):
|
||||
"""
|
||||
Load portal relations from MOPR data.
|
||||
Note that portals do not have to have a pair of relations. They may be linked to just one group.
|
||||
In that the portal works in a one-sided way. Example is floating cathedral exterior in Stormwind.
|
||||
"""
|
||||
|
||||
portal_relations = {}
|
||||
|
||||
for index, group in tqdm(list(enumerate(self.bl_groups)), desc='Importing portal relations', ascii=True):
|
||||
portal_start = group.wmo_group.mogp.portal_start
|
||||
portal_count = group.wmo_group.mogp.portal_count
|
||||
|
||||
if not portal_count:
|
||||
continue
|
||||
|
||||
for i in range(portal_count):
|
||||
relation = self.wmo.mopr.relations[portal_start + i]
|
||||
|
||||
# group from
|
||||
this_portal_rels = portal_relations.setdefault(relation.portal_index, (set(), set()))
|
||||
this_portal_rels[0].add(group.bl_object)
|
||||
this_portal_rels[1].add(group.bl_object)
|
||||
|
||||
other_group = self.bl_groups[relation.group_index]
|
||||
this_portal_rels[0].add(other_group.bl_object)
|
||||
|
||||
for portal_index, linked_groups in portal_relations.items():
|
||||
assert(len(linked_groups) == 2 and f"Portal links {len(linked_groups)} groups. Expected 2.")
|
||||
portal = self.bl_portals[portal_index]
|
||||
|
||||
l_linked_groups = list(linked_groups[0])
|
||||
portal.wow_wmo_portal.first = l_linked_groups[0]
|
||||
portal.wow_wmo_portal.second = l_linked_groups[1]
|
||||
|
||||
if portal.wow_wmo_portal.first not in linked_groups[1]:
|
||||
portal.wow_wmo_portal.detail = "1"
|
||||
elif portal.wow_wmo_portal.second not in linked_groups[1]:
|
||||
portal.wow_wmo_portal.detail = "2"
|
||||
|
||||
def load_properties(self):
|
||||
""" Load global WoW WMO properties """
|
||||
|
||||
wow_model_collection = get_current_wow_model_collection(bpy.context.scene, 'wow_wmo')
|
||||
|
||||
properties = wow_model_collection.wow_wmo
|
||||
properties.ambient_color = (self.wmo.mohd.ambient_color[0] / 255,
|
||||
self.wmo.mohd.ambient_color[1] / 255,
|
||||
self.wmo.mohd.ambient_color[2] / 255,
|
||||
self.wmo.mohd.ambient_color[3] / 255)
|
||||
|
||||
flags = set()
|
||||
if self.wmo.mohd.flags & 0x1:
|
||||
flags.add("0")
|
||||
if self.wmo.mohd.flags & 0x2:
|
||||
flags.add("2")
|
||||
if self.wmo.mohd.flags & 0x8:
|
||||
flags.add("1")
|
||||
# if self.wmo.mohd.flags & 0x4:
|
||||
# flags.add("3")
|
||||
|
||||
properties.flags = flags
|
||||
properties.skybox_path = self.wmo.mosb.skybox
|
||||
properties.wmo_id = self.wmo.mohd.id
|
||||
|
||||
def load_groups(self):
|
||||
|
||||
for i, group in tqdm(enumerate(self.wmo.groups), desc='Importing groups', ascii=True):
|
||||
bl_group = BlenderWMOSceneGroup(self, group)
|
||||
self.bl_groups.append(bl_group)
|
||||
|
||||
if not bl_group.name == 'antiportal':
|
||||
bl_group.load_object(i)
|
||||
|
||||
def build_references(self, export_selected, export_method):
|
||||
""" Build WMO references in Blender scene """
|
||||
|
||||
group_objects = []
|
||||
scn = bpy.context.scene
|
||||
|
||||
material_id = 0
|
||||
|
||||
sorted_objects = sorted(get_wmo_groups_list(scn), key=lambda obj: obj.wow_wmo_group.export_order)
|
||||
|
||||
for i, group_object in tqdm(enumerate(sorted_objects), desc='Building group references', ascii=True):
|
||||
if (export_selected and not group_object.select_get()) or group_object.hide_get():
|
||||
continue
|
||||
|
||||
group_object : bpy.types.Object
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
group_object.select_set(True)
|
||||
bpy.context.view_layer.objects.active = group_object
|
||||
|
||||
self.export_group_ids[group_object.name] = i
|
||||
|
||||
# self.groups_relations[group_object.name, relations]
|
||||
self.portals_relations[group_object] = []
|
||||
self.lights_relations[group_object] = []
|
||||
self.doodads_relations[group_object] = []
|
||||
|
||||
group = self.wmo.add_group()
|
||||
self.bl_groups.append(BlenderWMOSceneGroup(self, group, obj=group_object))
|
||||
group_objects.append(group_object)
|
||||
|
||||
# only iterate materials in the wmo groups.
|
||||
for material in group_object.data.materials:
|
||||
# don't create duplicates
|
||||
if material not in self.bl_materials.values():
|
||||
self.bl_materials[material_id] = material
|
||||
material_id += 1
|
||||
|
||||
group.export = not (export_method == 'PARTIAL' and not group_object.export)
|
||||
|
||||
|
||||
# process portals
|
||||
for i, portal_object in tqdm(enumerate(get_wmo_collection(scn, SpecialCollections.Portals).objects), desc='Building portal references', ascii=True):
|
||||
self.bl_portals.append(portal_object)
|
||||
portal_object.wow_wmo_portal.portal_id = i
|
||||
|
||||
# new system
|
||||
if portal_object.wow_wmo_portal.first:
|
||||
# self.groups_relations[portal_object.wow_wmo_portal.first.name].portals.append(portal_object.name)
|
||||
self.portals_relations[portal_object.wow_wmo_portal.first].append(portal_object)
|
||||
|
||||
if portal_object.wow_wmo_portal.second:
|
||||
# self.groups_relations[portal_object.wow_wmo_portal.second.name].portals.append(portal_object.name)
|
||||
self.portals_relations[portal_object.wow_wmo_portal.second].append(portal_object)
|
||||
|
||||
# process fogs
|
||||
for i, fog_object in tqdm(enumerate(get_wmo_collection(scn, SpecialCollections.Fogs).objects), desc='Building fog references', ascii=True):
|
||||
self.bl_fogs.append(fog_object)
|
||||
fog_object.wow_wmo_fog.fog_id = i
|
||||
|
||||
# process lights
|
||||
for i, light_object in tqdm(enumerate(get_wmo_collection(scn, SpecialCollections.Lights).objects), desc='Building light references', ascii=True):
|
||||
group = find_nearest_object(light_object, group_objects)
|
||||
self.lights_relations[group].append(i)
|
||||
self.bl_lights.append(light_object)
|
||||
|
||||
# process doodads
|
||||
doodad_counter = 0
|
||||
for i, doodad_set_collection in tqdm(enumerate(get_wmo_collection(scn, SpecialCollections.Doodads).children), desc='Building doodad references', ascii=True):
|
||||
|
||||
doodads = []
|
||||
|
||||
for doodad in doodad_set_collection.objects:
|
||||
group = find_nearest_object(doodad, group_objects)
|
||||
if group not in self.doodads_relations:
|
||||
print("ERROR doodad group ref, nearest_object: " + group.name)
|
||||
self.doodads_relations[group].append(doodad_counter)
|
||||
|
||||
doodad_counter += 1
|
||||
|
||||
doodads.append(doodad)
|
||||
|
||||
self.bl_doodad_sets[doodad_set_collection.name] = doodads
|
||||
|
||||
def save_materials(self):
|
||||
""" Add material if not already added, then return index in root file """
|
||||
|
||||
for i, mat in tqdm(enumerate(self.bl_materials.values())
|
||||
, desc='Saving materials'
|
||||
, ascii=True
|
||||
):
|
||||
|
||||
if not mat.wow_wmo_material.diff_texture_1:
|
||||
raise ReferenceError('\nError: Material \"{}\" must have a diffuse texture.'.format(mat.name))
|
||||
|
||||
diff_texture_1 = mat.wow_wmo_material.diff_texture_1.wow_wmo_texture.path
|
||||
diff_texture_1 = diff_texture_1.replace('/', '\\')
|
||||
diff_texture_1 = diff_texture_1.lower()
|
||||
|
||||
diff_texture_2 = mat.wow_wmo_material.diff_texture_2.wow_wmo_texture.path \
|
||||
if mat.wow_wmo_material.diff_texture_2 else ""
|
||||
diff_texture_2 = diff_texture_2.replace('/', '\\')
|
||||
diff_texture_2 = diff_texture_2.lower()
|
||||
|
||||
flags = 0
|
||||
|
||||
for flag in mat.wow_wmo_material.flags:
|
||||
flags |= int(flag)
|
||||
|
||||
self.wmo.add_material(diff_texture_1
|
||||
, diff_texture_2
|
||||
, int(mat.wow_wmo_material.shader)
|
||||
, int(mat.wow_wmo_material.blending_mode)
|
||||
, int(mat.wow_wmo_material.terrain_type)
|
||||
, flags
|
||||
, ( int(mat.wow_wmo_material.emissive_color[0] * 255),
|
||||
int(mat.wow_wmo_material.emissive_color[1] * 255),
|
||||
int(mat.wow_wmo_material.emissive_color[2] * 255),
|
||||
int(mat.wow_wmo_material.emissive_color[3] * 255)
|
||||
)
|
||||
, ( int(mat.wow_wmo_material.diff_color[2] * 255),
|
||||
int(mat.wow_wmo_material.diff_color[1] * 255),
|
||||
int(mat.wow_wmo_material.diff_color[0] * 255),
|
||||
int(mat.wow_wmo_material.diff_color[3] * 255)
|
||||
)
|
||||
)
|
||||
|
||||
def add_group_info(self, flags, bounding_box, name, desc):
|
||||
""" Add group info, then return offset of name and desc in a tuple """
|
||||
group_info = GroupInfo()
|
||||
|
||||
group_info.flags = flags # 8
|
||||
group_info.bounding_box_corner1 = [_ for _ in bounding_box[0]]
|
||||
group_info.bounding_box_corner2 = [_ for _ in bounding_box[1]]
|
||||
group_info.name_ofs = self.wmo.mogn.add_string(name) # 0xFFFFFFFF
|
||||
|
||||
if desc:
|
||||
desc_ofs = self.wmo.mogn.add_string(desc)
|
||||
else:
|
||||
desc_ofs = 0
|
||||
|
||||
self.wmo.mogi.infos.append(group_info)
|
||||
|
||||
return group_info.name_ofs, desc_ofs
|
||||
|
||||
def save_doodad_sets(self):
|
||||
""" Save doodads data from Blender scene to WMO root """
|
||||
|
||||
def normalize_quaternion(quat):
|
||||
w, x, y, z = quat
|
||||
norm = math.sqrt(w ** 2 + x ** 2 + y ** 2 + z ** 2)
|
||||
return (w / norm, x / norm, y / norm, z / norm)
|
||||
|
||||
has_global = False
|
||||
set_counter = 1
|
||||
|
||||
if len(self.bl_doodad_sets):
|
||||
|
||||
for set_name, doodads in tqdm(self.bl_doodad_sets.items(), desc='Saving doodad sets', ascii=True):
|
||||
|
||||
if set_name.startswith("Set_$DefaultGlobal"):
|
||||
if not has_global:
|
||||
set_name = "Set_$DefaultGlobal"
|
||||
has_global = True
|
||||
else:
|
||||
set_name = f"Set_{set_counter:03}"
|
||||
set_counter += 1
|
||||
|
||||
self.wmo.add_doodad_set(set_name, len(doodads))
|
||||
|
||||
for doodad in doodads:
|
||||
|
||||
path = doodad.wow_wmo_doodad.path.replace('/', '\\')
|
||||
|
||||
position = (doodad.matrix_world @ Vector((0, 0, 0))).to_tuple()
|
||||
|
||||
doodad.rotation_mode = 'QUATERNION'
|
||||
|
||||
doodad.rotation_quaternion = normalize_quaternion(doodad.rotation_quaternion)
|
||||
|
||||
rotation = (doodad.rotation_quaternion[1],
|
||||
doodad.rotation_quaternion[2],
|
||||
doodad.rotation_quaternion[3],
|
||||
doodad.rotation_quaternion[0])
|
||||
|
||||
scale = doodad.scale[0]
|
||||
|
||||
doodad_color = [int(channel * 255) for channel in doodad.wow_wmo_doodad.color]
|
||||
doodad_color = (doodad_color[2], doodad_color[1], doodad_color[0], doodad_color[3])
|
||||
|
||||
flags = 0
|
||||
for flag in doodad.wow_wmo_doodad.flags:
|
||||
flags |= int(flag)
|
||||
|
||||
self.wmo.add_doodad(path, position, rotation, scale, doodad_color, flags)
|
||||
|
||||
if not has_global:
|
||||
self.wmo.add_doodad_set("Set_$DefaultGlobal", 0)
|
||||
|
||||
def save_lights(self):
|
||||
|
||||
for obj in tqdm(self.bl_lights, desc='Saving lights', ascii=True):
|
||||
|
||||
light_type = int(obj.wow_wmo_light.light_type)
|
||||
|
||||
unk1 = obj.data.distance * 2
|
||||
|
||||
unk2 = obj.wow_wmo_light.type
|
||||
use_attenuation = obj.wow_wmo_light.use_attenuation
|
||||
padding = obj.wow_wmo_light.padding
|
||||
|
||||
color = (int(obj.wow_wmo_light.color[2] * 255),
|
||||
int(obj.wow_wmo_light.color[1] * 255),
|
||||
int(obj.wow_wmo_light.color[0] * 255),
|
||||
int(obj.wow_wmo_light.color_alpha * 255))
|
||||
|
||||
position = obj.location.to_tuple()
|
||||
intensity = obj.wow_wmo_light.intensity
|
||||
attenuation_start = obj.wow_wmo_light.attenuation_start
|
||||
attenuation_end = obj.wow_wmo_light.attenuation_end
|
||||
|
||||
self.wmo.add_light(light_type, unk1, unk2, use_attenuation, padding, color,
|
||||
position, intensity, attenuation_start, attenuation_end)
|
||||
|
||||
@staticmethod
|
||||
def get_angle(vec_a: Vector, vec_b: Vector, vec_n: Vector) -> float:
|
||||
return atan2(
|
||||
-(vec_a.x * vec_b.y * vec_n.z + vec_b.x * vec_n.y * vec_a.z + vec_n.x * vec_a.y * vec_b.z
|
||||
- vec_a.z * vec_b.y * vec_n.x - vec_b.z * vec_n.y * vec_a.x - vec_n.z * vec_a.y * vec_b.x),
|
||||
-(vec_a.x * vec_b.x + vec_a.y * vec_b.y + vec_a.z * vec_b.z)) + pi
|
||||
|
||||
@staticmethod
|
||||
def traverse(cur_vtx: BMVert
|
||||
, nodes_to_hit: List
|
||||
, nodes_hit: List
|
||||
, origin: BMVert) -> List:
|
||||
|
||||
for edge in cur_vtx.link_edges:
|
||||
|
||||
other = edge.other_vert(cur_vtx)
|
||||
|
||||
if other == origin:
|
||||
if not nodes_to_hit:
|
||||
return nodes_hit
|
||||
else:
|
||||
continue
|
||||
|
||||
if other in nodes_hit:
|
||||
continue
|
||||
|
||||
nodes_to_hit_new = nodes_to_hit.copy()
|
||||
nodes_to_hit_new.remove(other)
|
||||
|
||||
nodes_hit_new = nodes_hit.copy()
|
||||
nodes_hit_new.append(other)
|
||||
|
||||
result = BlenderWMOScene.traverse(other, nodes_to_hit_new, nodes_hit_new, origin)
|
||||
|
||||
if result:
|
||||
return result
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def sort_portal_vertices(vertices: List[BMVert], normal: Vector) -> List[BMVert]:
|
||||
|
||||
pos_n = Vector((0, 0, 0))
|
||||
origin = None
|
||||
|
||||
for vtx in vertices:
|
||||
if len(vtx.link_edges) == 2 and origin is None:
|
||||
origin = vtx
|
||||
pos_n += (vtx.co)
|
||||
|
||||
pos_n /= len(vertices)
|
||||
vtx_a = origin.link_edges[0].other_vert(origin)
|
||||
vtx_b = origin.link_edges[1].other_vert(origin)
|
||||
vector_o = (origin.co) - pos_n
|
||||
next_vtx = vtx_b if BlenderWMOScene.get_angle((vtx_a.co) - pos_n, vector_o, normal) \
|
||||
< BlenderWMOScene.get_angle((vtx_b.co) - pos_n, vector_o, normal) else vtx_a
|
||||
|
||||
# traversing mesh
|
||||
nodes_to_hit = list(vertices).copy()
|
||||
nodes_to_hit.remove(origin)
|
||||
nodes_to_hit.remove(next_vtx)
|
||||
|
||||
nodes_hit = [origin, next_vtx]
|
||||
result = BlenderWMOScene.traverse(next_vtx, nodes_to_hit, nodes_hit, origin)
|
||||
|
||||
return result
|
||||
|
||||
def save_portals(self):
|
||||
|
||||
saved_portals_ids = []
|
||||
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
self.wmo.mopt.infos = len(self.bl_portals) * [PortalInfo()]
|
||||
|
||||
for bl_group, group_mesh_eval in tqdm(zip(self.bl_groups, self.groups_eval), desc='Saving portals', ascii=True):
|
||||
|
||||
group_obj = bl_group.bl_object
|
||||
portal_relations = self.portals_relations[group_obj]
|
||||
bl_group.wmo_group.mogp.portal_start = len(self.wmo.mopr.relations)
|
||||
|
||||
for portal_obj in portal_relations:
|
||||
portal_index = portal_obj.original.wow_wmo_portal.portal_id
|
||||
portal_mesh = portal_obj.data
|
||||
|
||||
if portal_index not in saved_portals_ids:
|
||||
|
||||
portal_info = PortalInfo()
|
||||
portal_info.start_vertex = len(self.wmo.mopv.portal_vertices)
|
||||
v = []
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(portal_mesh)
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
portal_matrix_normal = portal_obj.matrix_world.to_3x3().transposed().inverted()
|
||||
portal_normal = (portal_matrix_normal @ portal_mesh.polygons[0].normal).normalized()
|
||||
|
||||
portal_verts = self.sort_portal_vertices(bm.verts, portal_mesh.polygons[0].normal)
|
||||
|
||||
for vertex in portal_verts:
|
||||
vertex_pos = portal_obj.matrix_world @ vertex.co
|
||||
self.wmo.mopv.portal_vertices.append(vertex_pos.to_tuple())
|
||||
v.append(vertex_pos)
|
||||
|
||||
bm.free()
|
||||
|
||||
v_A = v[0][1] * v[1][2] - v[1][1] * v[0][2] - v[0][1] * v[2][2] + v[2][1] * v[0][2] + v[1][1] * \
|
||||
v[2][2] - \
|
||||
v[2][1] * v[1][2]
|
||||
v_B = -v[0][0] * v[1][2] + v[2][0] * v[1][2] + v[1][0] * v[0][2] - v[2][0] * v[0][2] - v[1][0] * \
|
||||
v[2][2] + \
|
||||
v[0][0] * v[2][2]
|
||||
v_C = v[2][0] * v[0][1] - v[1][0] * v[0][1] - v[0][0] * v[2][1] + v[1][0] * v[2][1] - v[2][0] * \
|
||||
v[1][1] + \
|
||||
v[0][0] * v[1][1]
|
||||
v_D = -v[0][0] * v[1][1] * v[2][2] + v[0][0] * v[2][1] * v[1][2] + v[1][0] * v[0][1] * v[2][2] - \
|
||||
v[1][0] * \
|
||||
v[2][1] * v[0][2] - v[2][0] * v[0][1] * v[1][2] + v[2][0] * v[1][1] * v[0][2]
|
||||
|
||||
portal_info.unknown = v_D / sqrt(v_A * v_A + v_B * v_B + v_C * v_C)
|
||||
portal_info.n_vertices = len(self.wmo.mopv.portal_vertices) - portal_info.start_vertex
|
||||
portal_info.normal = portal_normal.to_tuple()
|
||||
|
||||
self.wmo.mopt.infos[portal_index] = portal_info
|
||||
saved_portals_ids.append(portal_index)
|
||||
|
||||
first = portal_obj.original.wow_wmo_portal.first
|
||||
second = portal_obj.original.wow_wmo_portal.second
|
||||
|
||||
# skip detail groups (see e.g. Stormwind cathedral)
|
||||
if portal_obj.original.wow_wmo_portal.detail != '0':
|
||||
if first.name == group_obj.original.name:
|
||||
if portal_obj.original.wow_wmo_portal.detail == '1':
|
||||
continue
|
||||
else:
|
||||
if portal_obj.original.wow_wmo_portal.detail == '2':
|
||||
continue
|
||||
|
||||
# calculating portal relation
|
||||
relation = PortalRelation()
|
||||
relation.portal_index = portal_index
|
||||
|
||||
relation.group_index = self.export_group_ids[second.name] if first.name == group_obj.original.name \
|
||||
else self.export_group_ids[first.name]
|
||||
|
||||
relation.side = bl_group.get_portal_direction(portal_obj, group_obj.evaluated_get(depsgraph))
|
||||
|
||||
self.wmo.mopr.relations.append(relation)
|
||||
|
||||
bl_group.wmo_group.mogp.portal_count = len(self.wmo.mopr.relations) - bl_group.wmo_group.mogp.portal_start
|
||||
|
||||
def prepare_groups(self):
|
||||
for bl_group in tqdm(self.bl_groups, desc='Preparing groups', ascii=True):
|
||||
if bl_group.wmo_group.export:
|
||||
bl_group.doodads_relations = self.doodads_relations[bl_group.bl_object]
|
||||
bl_group.lights_relations = self.lights_relations[bl_group.bl_object]
|
||||
|
||||
mesh, params = bl_group.create_batching_parameters()
|
||||
self.groups_eval.append(mesh)
|
||||
self.group_batch_params.append(params)
|
||||
|
||||
def save_groups(self):
|
||||
for _ in tqdm(range(1), desc='Processing group geometry', ascii=True):
|
||||
batcher = CWMOGeometryBatcher(self.group_batch_params)
|
||||
|
||||
for i, bl_group in enumerate(tqdm(self.bl_groups, desc='Saving groups', ascii=True)):
|
||||
|
||||
if bl_group.wmo_group.export:
|
||||
bl_group.save(batcher, i)
|
||||
|
||||
def save_fogs(self):
|
||||
|
||||
for fog_obj in tqdm(self.bl_fogs, desc='Saving fogs', ascii=True):
|
||||
|
||||
big_radius = fog_obj.dimensions[2] / 2
|
||||
# small radius % calculation is done by pywowlib
|
||||
|
||||
color1 = (int(fog_obj.wow_wmo_fog.color1[2] * 255),
|
||||
int(fog_obj.wow_wmo_fog.color1[1] * 255),
|
||||
int(fog_obj.wow_wmo_fog.color1[0] * 255),
|
||||
0xFF)
|
||||
|
||||
color2 = (int(fog_obj.wow_wmo_fog.color2[2] * 255),
|
||||
int(fog_obj.wow_wmo_fog.color2[1] * 255),
|
||||
int(fog_obj.wow_wmo_fog.color2[0] * 255),
|
||||
0xFF)
|
||||
|
||||
end_dist = fog_obj.wow_wmo_fog.end_dist
|
||||
end_dist2 = fog_obj.wow_wmo_fog.end_dist2
|
||||
position = fog_obj.location.to_tuple()
|
||||
start_factor = fog_obj.wow_wmo_fog.start_factor
|
||||
start_factor2 = fog_obj.wow_wmo_fog.start_factor2
|
||||
|
||||
flags = 0
|
||||
if fog_obj.wow_wmo_fog.ignore_radius:
|
||||
flags |= 0x01
|
||||
if fog_obj.wow_wmo_fog.unknown:
|
||||
flags |= 0x10
|
||||
|
||||
self.wmo.add_fog(big_radius, fog_obj.wow_wmo_fog.inner_radius, color1, color2, end_dist, end_dist2, position,
|
||||
start_factor, start_factor2, flags)
|
||||
|
||||
def save_root_header(self):
|
||||
|
||||
wow_model_collection = get_current_wow_model_collection(bpy.context.scene, 'wow_wmo')
|
||||
|
||||
properties = wow_model_collection.wow_wmo
|
||||
|
||||
scene = bpy.context.scene
|
||||
|
||||
self.wmo.mver.version = 17
|
||||
|
||||
# setting up default fog with default blizzlike values.
|
||||
if not len(self.wmo.mfog.fogs):
|
||||
empty_fog = Fog()
|
||||
empty_fog.color1 = (0xFF, 0xFF, 0xFF, 0xFF)
|
||||
empty_fog.color2 = (0x00, 0x00, 0x00, 0xFF)
|
||||
empty_fog.end_dist = 444.4445
|
||||
empty_fog.end_dist2 = 222.2222
|
||||
empty_fog.start_factor = 0.25
|
||||
empty_fog.start_factor2 = -0.5
|
||||
self.wmo.mfog.fogs.append(empty_fog)
|
||||
|
||||
bb = self.wmo.get_global_bounding_box()
|
||||
self.wmo.mohd.bounding_box_corner1 = bb[0]
|
||||
self.wmo.mohd.bounding_box_corner2 = bb[1]
|
||||
|
||||
# DBC foreign keys
|
||||
self.wmo.mohd.id = properties.wmo_id
|
||||
self.wmo.mosb.skybox = properties.skybox_path
|
||||
|
||||
self.wmo.mohd.ambient_color = [int(properties.ambient_color[0] * 255),
|
||||
int(properties.ambient_color[1] * 255),
|
||||
int(properties.ambient_color[2] * 255),
|
||||
int(properties.ambient_color[3] * 255)]
|
||||
|
||||
self.wmo.mohd.n_materials = len(self.wmo.momt.materials)
|
||||
self.wmo.mohd.n_groups = len(self.wmo.mogi.infos)
|
||||
self.wmo.mohd.n_portals = len(self.wmo.mopt.infos)
|
||||
self.wmo.mohd.n_models = self.wmo.modn.string_table.decode("ascii").count('.MDX')
|
||||
self.wmo.mohd.n_lights = len(self.wmo.molt.lights)
|
||||
self.wmo.mohd.n_doodads = len(self.wmo.modd.definitions)
|
||||
self.wmo.mohd.n_sets = len(self.wmo.mods.sets)
|
||||
|
||||
flags = properties.flags
|
||||
if "0" in flags:
|
||||
self.wmo.mohd.flags |= 0x01
|
||||
if "2" in flags:
|
||||
self.wmo.mohd.flags |= 0x02
|
||||
if "1" in flags:
|
||||
self.wmo.mohd.flags |= 0x08
|
||||
# if "3" in flags:
|
||||
# self.wmo.mohd.flags |= 0x4
|
||||
version = int(scene.wow_scene.version)
|
||||
if version >= WoWVersions.WOTLK:
|
||||
self.wmo.mohd.flags |= 0x4
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+1233
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user