diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..c9ebf2d2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system" +} \ No newline at end of file diff --git a/blender_bindings/material_loader/material_loader.py b/blender_bindings/material_loader/material_loader.py index abc40cb4..67d4e0d1 100644 --- a/blender_bindings/material_loader/material_loader.py +++ b/blender_bindings/material_loader/material_loader.py @@ -112,7 +112,6 @@ def _initial_setup(cls, material: bpy.types.Material): if material.get('source_loaded', False): return False - material.use_nodes = True material['source_loaded'] = True material.use_nodes = True cls._clean_nodes(material) diff --git a/blender_bindings/material_loader/shader_base.py b/blender_bindings/material_loader/shader_base.py index b0943360..7739a3c4 100644 --- a/blender_bindings/material_loader/shader_base.py +++ b/blender_bindings/material_loader/shader_base.py @@ -73,7 +73,8 @@ class Nodes: ShaderNodeRGBToBW = 'ShaderNodeRGBToBW' ShaderNodeScript = 'ShaderNodeScript' ShaderNodeSeparateHSV = 'ShaderNodeSeparateHSV' - ShaderNodeSeparateRGB = 'ShaderNodeSeparateRGB' + ShaderNodeSeparateColor = 'ShaderNodeSeparateColor' + ShaderNodeSeparateRGB = 'ShaderNodeSeparateColor' ShaderNodeSeparateXYZ = 'ShaderNodeSeparateXYZ' ShaderNodeShaderToRGB = 'ShaderNodeShaderToRGB' ShaderNodeSqueeze = 'ShaderNodeSqueeze' diff --git a/blender_bindings/material_loader/shaders/source1_shaders/heroes_armor.py b/blender_bindings/material_loader/shaders/source1_shaders/heroes_armor.py index 4822ff77..5918a50e 100644 --- a/blender_bindings/material_loader/shaders/source1_shaders/heroes_armor.py +++ b/blender_bindings/material_loader/shaders/source1_shaders/heroes_armor.py @@ -360,7 +360,7 @@ def create_nodes(self, material: bpy.types.Material, extra_parameters: dict[Extr elif self.phongexponenttexture is not None: phongexponenttexture_node = self.create_node(Nodes.ShaderNodeTexImage, '$phongexponenttexture') phongexponenttexture_node.image = phongexponenttexture - phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateRGB) + phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(phongexponenttexture_node.outputs['Color'], phongexponenttexture_split_node.inputs['Image']) diff --git a/blender_bindings/material_loader/shaders/source1_shaders/heroes_faceskin.py b/blender_bindings/material_loader/shaders/source1_shaders/heroes_faceskin.py index 05b85061..17c44f68 100644 --- a/blender_bindings/material_loader/shaders/source1_shaders/heroes_faceskin.py +++ b/blender_bindings/material_loader/shaders/source1_shaders/heroes_faceskin.py @@ -361,7 +361,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra elif self.phongexponenttexture is not None: phongexponenttexture_node = self.create_node(Nodes.ShaderNodeTexImage, '$phongexponenttexture') phongexponenttexture_node.image = phongexponenttexture - phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateRGB) + phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(phongexponenttexture_node.outputs['Color'], phongexponenttexture_split_node.inputs['Image']) diff --git a/blender_bindings/material_loader/shaders/source1_shaders/heroes_hair.py b/blender_bindings/material_loader/shaders/source1_shaders/heroes_hair.py index ac2db2f0..8921ce1b 100644 --- a/blender_bindings/material_loader/shaders/source1_shaders/heroes_hair.py +++ b/blender_bindings/material_loader/shaders/source1_shaders/heroes_hair.py @@ -365,7 +365,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra elif self.phongexponenttexture is not None: phongexponenttexture_node = self.create_node(Nodes.ShaderNodeTexImage, '$phongexponenttexture') phongexponenttexture_node.image = phongexponenttexture - phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateRGB) + phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(phongexponenttexture_node.outputs['Color'], phongexponenttexture_split_node.inputs['Image']) diff --git a/blender_bindings/material_loader/shaders/source1_shaders/heroes_pbs.py b/blender_bindings/material_loader/shaders/source1_shaders/heroes_pbs.py index e83b3636..83c24de6 100644 --- a/blender_bindings/material_loader/shaders/source1_shaders/heroes_pbs.py +++ b/blender_bindings/material_loader/shaders/source1_shaders/heroes_pbs.py @@ -368,7 +368,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra elif self.phongexponenttexture is not None: phongexponenttexture_node = self.create_node(Nodes.ShaderNodeTexImage, '$phongexponenttexture') phongexponenttexture_node.image = phongexponenttexture - phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateRGB) + phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(phongexponenttexture_node.outputs['Color'], phongexponenttexture_split_node.inputs['Image']) diff --git a/blender_bindings/material_loader/shaders/source1_shaders/infected.py b/blender_bindings/material_loader/shaders/source1_shaders/infected.py index fa0c25d0..168582e0 100644 --- a/blender_bindings/material_loader/shaders/source1_shaders/infected.py +++ b/blender_bindings/material_loader/shaders/source1_shaders/infected.py @@ -406,7 +406,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra elif self.phongexponenttexture is not None: phongexponenttexture_node = self.create_node(Nodes.ShaderNodeTexImage, '$phongexponenttexture') phongexponenttexture_node.image = phongexponenttexture - phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateRGB) + phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(phongexponenttexture_node.outputs['Color'], phongexponenttexture_split_node.inputs['Image']) diff --git a/blender_bindings/material_loader/shaders/source1_shaders/vertexlit_generic.py b/blender_bindings/material_loader/shaders/source1_shaders/vertexlit_generic.py index 8498b05e..3078beb8 100644 --- a/blender_bindings/material_loader/shaders/source1_shaders/vertexlit_generic.py +++ b/blender_bindings/material_loader/shaders/source1_shaders/vertexlit_generic.py @@ -30,6 +30,46 @@ def basetexture(self): return self.load_texture_or_default(texture_path, (0.3, 0, 0.3, 1.0)) return None + @property + def compress(self): + texture_path = self._vmt.get_string('$compress', None) + if texture_path is not None: + return self.load_texture_or_default(texture_path, (0.3, 0.0, 0.3, 1.0)) + return None + + @property + def stretch(self): + texture_path = self._vmt.get_string('$stretch', None) + if texture_path is not None: + return self.load_texture_or_default(texture_path, (0.3, 0.0, 0.3, 1.0)) + return None + + @property + def bumpcompress(self): + texture_path = self._vmt.get_string('$bumpcompress', None) + if texture_path is not None: + image = self.load_texture_or_default(texture_path, (0.5, 0.5, 1.0, 1.0)) + if image == self.basetexture: + return image + image = self.convert_normalmap(image) + image.colorspace_settings.is_data = True + image.colorspace_settings.name = 'Non-Color' + return image + return None + + @property + def bumpstretch(self): + texture_path = self._vmt.get_string('$bumpstretch', None) + if texture_path is not None: + image = self.load_texture_or_default(texture_path, (0.5, 0.5, 1.0, 1.0)) + if image == self.basetexture: + return image + image = self.convert_normalmap(image) + image.colorspace_settings.is_data = True + image.colorspace_settings.name = 'Non-Color' + return image + return None + @property def lightwarptexture(self): texture_path = self._vmt.get_string('$lightwarptexture', None) @@ -245,7 +285,7 @@ def phongtint(self): def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[ExtraMaterialParameters, Any]): sio_diffuse = None - selfillum = False + tension = None # print(f"BVLG: {self.use_bvlg_status}") @@ -322,6 +362,24 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra if self.detail: albedo, detail = self.handle_detail(group_node.inputs['$basetexture [texture]'], albedo, uv_node=uv) + if self.compress and self.stretch: + attr = self.create_node(Nodes.ShaderNodeAttribute) + attr.attribute_name = 'tension' + tension = self.create_node(Nodes.ShaderNodeSeparateXYZ) + self.connect_nodes(attr.outputs[1], tension.inputs[0]) + compress = self.create_texture_node(self.compress) + stretch = self.create_texture_node(self.stretch) + mix_default_compress = self.create_node(Nodes.ShaderNodeMixRGB) + mix_compress_stretch = self.create_node(Nodes.ShaderNodeMixRGB) + + self.connect_nodes(tension.outputs[0], mix_default_compress.inputs[0]) + self.connect_nodes(tension.outputs[1], mix_compress_stretch.inputs[0]) + self.connect_nodes(albedo, mix_default_compress.inputs[1]) + self.connect_nodes(compress.outputs[0], mix_default_compress.inputs[2]) + self.connect_nodes(mix_default_compress.outputs[0], mix_compress_stretch.inputs[1]) + self.connect_nodes(stretch.outputs[0], mix_compress_stretch.inputs[2]) + self.connect_nodes(mix_compress_stretch.outputs[0], group_node.inputs['$basetexture [texture]']) + elif self.color: group_node.inputs['$basetexture [texture]'].default_value = self.color @@ -344,6 +402,24 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra elif self.normalmapalphaphongmask and not self.basemapalphaphongmask: self.connect_nodes(bumpmap_node.outputs['Alpha'], group_node.inputs['phongmask [bumpmap texture alpha]']) + + if self.bumpcompress and self.bumpstretch: + if not tension: + attr = self.create_node(Nodes.ShaderNodeAttribute) + tension = self.create_node(Nodes.ShaderNodeSeparateXYZ) + self.connect_nodes(attr.outputs[1], tension.inputs[0]) + compress = self.create_texture_node(self.bumpcompress) + stretch = self.create_texture_node(self.bumpstretch) + mix_default_compress = self.create_node(Nodes.ShaderNodeMixRGB) + mix_compress_stretch = self.create_node(Nodes.ShaderNodeMixRGB) + + self.connect_nodes(tension.outputs[0], mix_default_compress.inputs[0]) + self.connect_nodes(tension.outputs[1], mix_compress_stretch.inputs[0]) + self.connect_nodes(bumpmap_node.outputs[0], mix_default_compress.inputs[1]) + self.connect_nodes(compress.outputs[0], mix_default_compress.inputs[2]) + self.connect_nodes(mix_default_compress.outputs[0], mix_compress_stretch.inputs[1]) + self.connect_nodes(stretch.outputs[0], mix_compress_stretch.inputs[2]) + self.connect_nodes(mix_compress_stretch.outputs[0], group_node.inputs['$bumpmap [texture]']) if self.rimlight: group_node.inputs['$rimlight [bool]'].default_value = self.rimlight @@ -568,7 +644,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra elif self.phongexponenttexture is not None: phongexponenttexture_node = self.create_node(Nodes.ShaderNodeTexImage, '$phongexponenttexture') phongexponenttexture_node.image = phongexponenttexture - phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateRGB) + phongexponenttexture_split_node = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(phongexponenttexture_node.outputs['Color'], phongexponenttexture_split_node.inputs['Image']) diff --git a/blender_bindings/material_loader/shaders/source1_shaders/worldvertextransition.py b/blender_bindings/material_loader/shaders/source1_shaders/worldvertextransition.py index d770c6d4..c2d10311 100644 --- a/blender_bindings/material_loader/shaders/source1_shaders/worldvertextransition.py +++ b/blender_bindings/material_loader/shaders/source1_shaders/worldvertextransition.py @@ -107,7 +107,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra bs_socket2 = bs_node2.outputs[0] if self.blendmodulatetexture != None: - SEPrgb = self.create_node(Nodes.ShaderNodeSeparateRGB) + SEPrgb = self.create_node(Nodes.ShaderNodeSeparateColor) sub = self.create_node(Nodes.ShaderNodeMath) sub.operation = 'SUBTRACT' add = self.create_node(Nodes.ShaderNodeMath) diff --git a/blender_bindings/material_loader/shaders/source2_shaders/blend.py b/blender_bindings/material_loader/shaders/source2_shaders/blend.py index 1e624688..61e73fd6 100644 --- a/blender_bindings/material_loader/shaders/source2_shaders/blend.py +++ b/blender_bindings/material_loader/shaders/source2_shaders/blend.py @@ -141,7 +141,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra self.connect_nodes(transform_node.outputs[0], normal2_node.inputs[0]) self.connect_nodes(transform_node.outputs[0], mask_node.inputs[0]) - split_mask = self.create_node(Nodes.ShaderNodeSeparateRGB) + split_mask = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(mask_node.outputs[0], split_mask.inputs[0]) mask_output = split_mask.outputs[0] diff --git a/blender_bindings/material_loader/shaders/source2_shaders/csgo_environment_blend.py b/blender_bindings/material_loader/shaders/source2_shaders/csgo_environment_blend.py index 9fa4770b..27495a0b 100644 --- a/blender_bindings/material_loader/shaders/source2_shaders/csgo_environment_blend.py +++ b/blender_bindings/material_loader/shaders/source2_shaders/csgo_environment_blend.py @@ -51,13 +51,13 @@ def create_nodes(self, material: bpy.types.Material, extra_parameters: dict[Extr if self._have_texture("g_tHeight1"): color_texture = self._get_texture("g_tHeight1", (1, 1, 1, 1), True) - split = self.create_node(Nodes.ShaderNodeSeparateRGB) + split = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(color_texture.outputs[0], split.inputs[0]) self.connect_nodes(split.outputs[0], shader.inputs["V0"]) if self._have_texture("g_tHeight2"): color_texture = self._get_texture("g_tHeight2", (1, 1, 1, 1), True) - split = self.create_node(Nodes.ShaderNodeSeparateRGB) + split = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(color_texture.outputs[0], split.inputs[0]) self.connect_nodes(split.outputs[0], shader.inputs["V1"]) diff --git a/blender_bindings/material_loader/shaders/source2_shaders/csgo_unlitgeneric.py b/blender_bindings/material_loader/shaders/source2_shaders/csgo_unlitgeneric.py index f40ced58..f663a2a9 100644 --- a/blender_bindings/material_loader/shaders/source2_shaders/csgo_unlitgeneric.py +++ b/blender_bindings/material_loader/shaders/source2_shaders/csgo_unlitgeneric.py @@ -29,7 +29,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra self.connect_nodes(normal_texture.outputs[1], shader.inputs["Roughness"]) # metalness_texture = self._get_texture("g_tMetalness", (1, 1, 1, 1), True) - # metalness_conv = self.create_node(Nodes.ShaderNodeSeparateRGB) + # metalness_conv = self.create_node(Nodes.ShaderNodeSeparateColor) # self.connect_nodes(metalness_texture.outputs[0], metalness_conv.inputs[0]) # roughness_override = self._material_resource.get_vector_property("TextureNormal", None) # if roughness_override is not None: diff --git a/blender_bindings/material_loader/shaders/source2_shaders/csgo_weapon.py b/blender_bindings/material_loader/shaders/source2_shaders/csgo_weapon.py index e142312f..82ff8bcf 100644 --- a/blender_bindings/material_loader/shaders/source2_shaders/csgo_weapon.py +++ b/blender_bindings/material_loader/shaders/source2_shaders/csgo_weapon.py @@ -37,7 +37,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra self.connect_nodes(normal_texture.outputs[1], shader.inputs["Specular"]) metalness_texture = self._get_texture("g_tMetalness", (1, 1, 1, 1), True) - metalness_conv = self.create_node(Nodes.ShaderNodeSeparateRGB) + metalness_conv = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(metalness_texture.outputs[0], metalness_conv.inputs[0]) self.connect_nodes(metalness_conv.outputs[0], shader.inputs["Roughness"]) self.connect_nodes(metalness_conv.outputs[1], shader.inputs["Metallic"]) diff --git a/blender_bindings/material_loader/shaders/source2_shaders/pbr.py b/blender_bindings/material_loader/shaders/source2_shaders/pbr.py index 7388bfdf..d3aaf56a 100644 --- a/blender_bindings/material_loader/shaders/source2_shaders/pbr.py +++ b/blender_bindings/material_loader/shaders/source2_shaders/pbr.py @@ -193,7 +193,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra uv_mapping = self.setup_uv_transform(self.selfillum_texcoord_offset1, self.selfillum_texcoord_scale1) if uv_mapping: self.connect_nodes(uv_mapping.outputs[0], selfillum_mask_node.inputs[0]) - split_node = self.create_node(Nodes.ShaderNodeSeparateRGB) + split_node = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(selfillum_mask_node.outputs[0], split_node.inputs[0]) if self.selfillum_scale == 1.0: self.connect_nodes(split_node.outputs[0], shader.inputs["Emission Strength"]) diff --git a/blender_bindings/material_loader/shaders/source2_shaders/vr_complex.py b/blender_bindings/material_loader/shaders/source2_shaders/vr_complex.py index 7bf0a9ec..9a84074f 100644 --- a/blender_bindings/material_loader/shaders/source2_shaders/vr_complex.py +++ b/blender_bindings/material_loader/shaders/source2_shaders/vr_complex.py @@ -70,7 +70,7 @@ def create_nodes(self, material: bpy.types.Material, extra_parameters: dict[Extr tint_mask_output = None if self._check_flag("F_TINT_MASK"): tint_mask_node = self._get_texture("g_tTintMask", (1.0, 1.0, 1.0, 1.0), True) - split_rgb_node = self.create_node(Nodes.ShaderNodeSeparateRGB) + split_rgb_node = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(tint_mask_node.outputs['Color'], split_rgb_node.inputs[0]) tint_mask_output = split_rgb_node.outputs[0] diff --git a/blender_bindings/material_loader/shaders/source2_shaders/vr_simple.py b/blender_bindings/material_loader/shaders/source2_shaders/vr_simple.py index 4aef92c5..c918a7d1 100644 --- a/blender_bindings/material_loader/shaders/source2_shaders/vr_simple.py +++ b/blender_bindings/material_loader/shaders/source2_shaders/vr_simple.py @@ -107,7 +107,7 @@ def create_nodes(self, material:bpy.types.Material, extra_parameters: dict[Extra tint_mask_output = None if self._check_flag("F_TINT_MASK"): tint_mask_node = self._get_texture("g_tTintMask", (1.0, 1.0, 1.0, 1.0), True) - split_rgb_node = self.create_node(Nodes.ShaderNodeSeparateRGB) + split_rgb_node = self.create_node(Nodes.ShaderNodeSeparateColor) self.connect_nodes(tint_mask_node.outputs['Color'], split_rgb_node.inputs[0]) tint_mask_output = split_rgb_node.outputs[0] diff --git a/blender_bindings/models/common.py b/blender_bindings/models/common.py index 002c748a..94621fc1 100644 --- a/blender_bindings/models/common.py +++ b/blender_bindings/models/common.py @@ -53,6 +53,8 @@ def put_into_collections(model_container: ModelContainer, model_name, body_part_collection = master_collection for mesh in meshes: + if mesh == None: + continue body_collection = get_new_unique_collection(mesh.name, body_part_collection) body_collection.objects.link(mesh) else: @@ -155,4 +157,150 @@ def get_obj_transforms_driver(drivers, transform_type, do_quaternion=False): if nodes.get('!EYE_Z'): nodes['!EYE_Z'].attribute_name = eyeball_name + '_z_offset' if nodes.get('!EYE_IRIS_SCALE'): - nodes['!EYE_IRIS_SCALE'].attribute_name = eyeball_name + '_iris_scale' \ No newline at end of file + nodes['!EYE_IRIS_SCALE'].attribute_name = eyeball_name + '_iris_scale' + +def make_bodygroup_selectors(mdl: Mdl, armature: bpy.types.Object, bodygroups: dict[str, list[bpy.types.Object]]): + from string import ascii_lowercase + + def add_vis_drivers( + controller: bpy.types.Object, + subject: bpy.types.Object, + data_path: str, + index: int + ): + controller.update_tag() + for path in ['hide_viewport', 'hide_render']: + subject.driver_remove(path) + curve = subject.driver_add(path) + driver = curve.driver + driver.type = 'SCRIPTED' + var = driver.variables.new() + targs = var.targets[0] + targs.id_type = 'OBJECT' + targs.id = controller + targs.data_path = f'["{data_path}"]' + driver.expression = f'var != {index}' + + bg_name_map = dict() + tally = iter(range(999)) + + def tally(): + for i in range(999): + yield ''.join(map(lambda a: ascii_lowercase[int(a)], f'{i}')) + tally = tally() + + for n, body_part in enumerate(mdl.body_parts): + if len(body_part.models) < 2: + continue + + enum_items = [] + bg_name = body_part.name + bg_name_suffix = 'BG' + ' ' + next(tally) + ' ' + bg_name + bg_name_map[bg_name] = bg_name_suffix + + armature[bg_name_suffix] = 0 + + for index, (bpy_model, model) in enumerate(zip(bodygroups[bg_name], body_part.models)): + enum_items.append(( + f'{n}', + model.name, + '' + )) + if bpy_model == None: + continue + add_vis_drivers( + armature, + bpy_model, + bg_name_suffix, + index + ) + + ui_settings = armature.id_properties_ui(bg_name_suffix) + ui_settings.update( + min=0, + max=len(enum_items), + items=enum_items + ) + + armature['bodygroup_name_map'] = bg_name_map + + +def generate_wrinkle_map_node_group(obj: bpy.types.Object): + data: bpy.types.Mesh = obj.data + shape_keys = data.shape_keys + + compress = list(filter(lambda a: a.name.startswith('WR.') and a.name.endswith('.C'), data.attributes)) + stretch = list(filter(lambda a: a.name.startswith('WR.') and a.name.endswith('.S'), data.attributes)) + + if not len(compress) + len(stretch): + return + + node_group: bpy.types.GeometryNodeTree = bpy.data.node_groups.new(f'wrinkles_{obj.name}'[:63], 'GeometryNodeTree') + nodes = node_group.nodes + links = node_group.links + mod: bpy.types.NodesModifier = obj.modifiers.new('Wrinkle Map Data', 'NODES') + mod.node_group = node_group + if bpy.app.version >= (4, 0, 0): + node_group.interface.new_socket(name='Output', in_out='OUTPUT', socket_type='NodeSocketGeometry') + node_group.interface.new_socket(name='Input', in_out='INPUT', socket_type='NodeSocketGeometry') + else: + node_group.inputs.new('NodeSocketGeometry', 'Input') + node_group.outputs.new('NodeSocketGeometry', 'Output') + + input = nodes.new('NodeGroupInput') + input.location = [400, 100] + output = nodes.new('NodeGroupOutput') + output.location = [800, 0] + combine = nodes.new('ShaderNodeCombineXYZ') + combine.location = [400, 0] + store = nodes.new('GeometryNodeStoreNamedAttribute') + store.data_type = 'FLOAT2' + store.domain = 'POINT' + store.inputs[2].default_value = 'tension' + store.location = [600, 0] + + links.new(combine.outputs[0], store.inputs[3]) + links.new(input.outputs[0], store.inputs[0]) + links.new(store.outputs[0], output.inputs[0]) + + + loc_compress = [0, 0] + loc_stretch = [200, 0] + + for n, attr in [*enumerate(compress), *enumerate(stretch)]: + loc, index = (loc_compress, 0) if attr.name.endswith('.C') else (loc_stretch, 1) + shape_name = attr.name.split('.')[1] + + wrinkle = nodes.new('GeometryNodeInputNamedAttribute') + wrinkle.inputs[0].default_value = attr.name + wrinkle.location = loc + wrinkle.name = 'WRINKLE MAP' + wrinkle.label = shape_name + + mult = nodes.new('ShaderNodeMath') + mult.operation = 'MULTIPLY' + mult.location = loc + mult.name = 'SHAPEKEY VALUE' + mult.label = shape_name + driver = mult.inputs[1].driver_add('default_value') + var = driver.driver.variables.new() + targ = var.targets[0] + targ.id_type = 'KEY' + targ.id = shape_keys + targ.data_path = shape_keys.key_blocks[shape_name].path_from_id('value') + driver.driver.type = 'AVERAGE' + + links.new(wrinkle.outputs[0], mult.inputs[0]) + + if n == 0: + last = mult.outputs[0] + links.new(last, combine.inputs[index]) + continue + maximum = nodes.new('ShaderNodeMath') + maximum.name = 'MAXIMUM' + maximum.operation = 'MAXIMUM' + maximum.location = loc + links.new(last, maximum.inputs[0]) + links.new(mult.outputs[0], maximum.inputs[1]) + links.new(maximum.outputs[0], combine.inputs[index]) + last = maximum.outputs[0] \ No newline at end of file diff --git a/blender_bindings/models/mdl44/__init__.py b/blender_bindings/models/mdl44/__init__.py index fa6312cc..792b213f 100644 --- a/blender_bindings/models/mdl44/__init__.py +++ b/blender_bindings/models/mdl44/__init__.py @@ -46,7 +46,7 @@ def import_mdl44(model_path: TinyPath, buffer: Buffer, logger.error(f'Failed to import materials, caused by {t_ex}') import traceback traceback.print_exc() - container = import_model(content_manager, mdl, vtx, vvd, options.scale, options.create_flex_drivers) + container = import_model(content_manager, mdl, vtx, vvd, options) if options.import_physics: phy_buffer = content_manager.find_file(model_path.with_suffix(".phy")) if phy_buffer is None: diff --git a/blender_bindings/models/mdl44/import_mdl.py b/blender_bindings/models/mdl44/import_mdl.py index d9d54b81..7b23cc3f 100644 --- a/blender_bindings/models/mdl44/import_mdl.py +++ b/blender_bindings/models/mdl44/import_mdl.py @@ -6,8 +6,9 @@ import numpy as np from mathutils import Euler, Matrix, Quaternion, Vector -from SourceIO.blender_bindings.models.common import merge_meshes, create_eyeballs +from SourceIO.blender_bindings.models.common import merge_meshes, create_eyeballs, generate_wrinkle_map_node_group from SourceIO.blender_bindings.shared.model_container import ModelContainer +from SourceIO.blender_bindings.operators.import_settings_base import ModelOptions from SourceIO.blender_bindings.utils.bpy_utils import add_material, is_blender_4_1, get_or_create_material from SourceIO.blender_bindings.utils.fast_mesh import FastMesh from SourceIO.library.models.mdl.structs.header import StudioHDRFlags @@ -79,7 +80,7 @@ def create_armature(mdl: MdlV44, scale=1.0, load_refpose=False): def import_model(content_manager: ContentManager, mdl: MdlV44, vtx: Vtx, vvd: Vvd, - scale=1.0, create_drivers=False, load_refpose=False): + options: ModelOptions): full_material_names = collect_full_material_names([mat.name for mat in mdl.materials], mdl.materials_paths, content_manager) [setattr(mat, 'bpy_material', get_or_create_material(mat.name, full_material_names[mat.name])) for mat in mdl.materials if mat.bpy_material is None] @@ -95,6 +96,11 @@ def import_model(content_manager: ContentManager, mdl: MdlV44, vtx: Vtx, vvd: Vv static_prop = mdl.header.flags & StudioHDRFlags.STATIC_PROP != 0 armature = None vertex_anim_cache = preprocess_vertex_animation(mdl, vvd) + vert_anim_fixed_point_scale = mdl.header.vert_anim_fixed_point_scale if (mdl.header.flags & StudioHDRFlags.VERT_ANIM_FIXED_POINT_SCALE !=0 ) else 1/4096 + + scale = options.scale + create_drivers = options.create_flex_drivers + debug_stereo_balance = options.debug_stereo_balance if not static_prop: armature = create_armature(mdl, scale) @@ -167,28 +173,76 @@ def import_model(content_manager: ContentManager, mdl: MdlV44, vtx: Vtx, vvd: Vv bone_name = mdl.bones[bone_index].name weight_groups[bone_name].add([n], weight, 'REPLACE') - flex_names = [] + flexes = [] for mesh in model.meshes: if mesh.flexes: - flex_names.extend([mdl.flex_names[flex.flex_desc_index] for flex in mesh.flexes]) + flexes.extend([(mdl.flex_names[flex.flex_desc_index], flex) for flex in mesh.flexes]) - if flex_names: + if flexes: mesh_obj.shape_key_add(name='base') - for flex_name in flex_names: - shape_key = mesh_data.shape_keys.key_blocks.get(flex_name, None) or mesh_obj.shape_key_add( - name=flex_name) - shape_key.value = 0.0 - vertex_animation = vertex_anim_cache[flex_name] - model_vertices = get_slice(vertex_animation["pos"], model.vertex_offset, model.vertex_count) - flex_vertices = model_vertices[vtx_vertices] * scale + if debug_stereo_balance: + # debug tool to get stereo flex balances. i'll leave it here just in case + side_right = mesh_obj.vertex_groups.new(name='blendright') + side_left = mesh_obj.vertex_groups.new(name='blendleft') + side_all = np.zeros(model.vertex_count, dtype=np.float32) + + for flex_name, flex_desc in flexes: + vertex_animation = vertex_anim_cache[flex_name] + side = get_slice(vertex_animation['side'], model.vertex_offset, model.vertex_count).ravel() + side_all = np.maximum(side_all, side) + side_all = side_all[vtx_vertices] + 0.0 - shape_key.data.foreach_set("co", flex_vertices.reshape(-1)) + for n, vert in enumerate(vtx_vertices): + side_right.add([n], side_all[n], 'REPLACE') + side_left.add([n], 1-side_all[n], 'REPLACE') + + for flex_name, flex_desc in flexes: + vertex_animation = vertex_anim_cache[flex_name] + flex_delta = get_slice(vertex_animation["pos"], model.vertex_offset, model.vertex_count) + flex_delta = flex_delta[vtx_vertices] * scale + + side = get_slice(vertex_animation["side"], model.vertex_offset, model.vertex_count) + side = side[vtx_vertices] + 0.0 + wrinkle = get_slice(vertex_animation["wrinkle"], model.vertex_offset, model.vertex_count) + wrinkle = wrinkle[vtx_vertices] + 0.0 # this will have to be explained to me :P + # model.vertex_count and vtx_vertices can differ in size, so doing something like this just makes it work? + # apparently vtx_vertices has duplicate indicies, which can be observed by turning it into a set. + # i'm just following here + # -hisanimations + + model_vertices = get_slice(all_vertices['vertex'], model.vertex_offset, model.vertex_count) + model_vertices = model_vertices[vtx_vertices] * scale + + if flex_desc.partner_index: + partner_name = mdl.flex_names[flex_desc.partner_index] + flexes, sides = [flex_name, partner_name], [1-side, side] if not debug_stereo_balance else [1.0, 1.0] + else: + flexes, sides = [flex_name], [1.0] + + for flex_name, side in zip(flexes, sides): + shape_key = mesh_data.shape_keys.key_blocks.get(flex_name, None) or mesh_obj.shape_key_add( + name=flex_name) + shape_key.data.foreach_set("co", (flex_delta*side + model_vertices).ravel()) + shape_key.value = 0.0 + + if flex_desc.vertex_anim_type == 1: + mesh_data: bpy.types.Mesh + if wrinkle.max() > 0: + wrinkle_name = f'WR.{flex_name}.S' + if wrinkle.min() < 0: + wrinkle_name = f'WR.{flex_name}.C' + attr: bpy.types.Attribute = mesh_data.attributes.get(wrinkle_name, None) or mesh_data.attributes.new(wrinkle_name, 'FLOAT', 'POINT') + wrinkle_data = (abs(wrinkle) * vert_anim_fixed_point_scale) * side + attr.data.foreach_set('value', wrinkle_data.ravel()) if create_drivers: create_flex_drivers(mesh_obj, mdl) - mesh_data.validate() + if options.generate_wrinkle_map_node_group: + generate_wrinkle_map_node_group(mesh_obj) + + mesh_data.validate() if model.has_eyeballs: create_eyeballs(mdl, armature, mesh_obj, model, scale, extra_stuff) diff --git a/blender_bindings/models/mdl49/__init__.py b/blender_bindings/models/mdl49/__init__.py index 074eab68..e400e7a1 100644 --- a/blender_bindings/models/mdl49/__init__.py +++ b/blender_bindings/models/mdl49/__init__.py @@ -45,7 +45,7 @@ def import_mdl49(model_path: TinyPath, buffer: Buffer, import traceback traceback.print_exc() - container = import_model(content_manager, mdl, vtx, vvd, options.scale, options.create_flex_drivers) + container = import_model(content_manager, mdl, vtx, vvd, options) if options.import_physics: phy_buffer = content_manager.find_file(model_path.with_suffix(".phy")) if phy_buffer is None: diff --git a/blender_bindings/models/mdl49/import_mdl copy.py b/blender_bindings/models/mdl49/import_mdl copy.py new file mode 100644 index 00000000..b65348c6 --- /dev/null +++ b/blender_bindings/models/mdl49/import_mdl copy.py @@ -0,0 +1,543 @@ +import warnings +from collections import defaultdict + +import bpy +import numpy as np +from mathutils import Euler, Matrix, Quaternion, Vector +from math import atan + +from SourceIO.blender_bindings.models.common import merge_meshes, create_eyeballs, generate_wrinkle_map_node_group +from SourceIO.blender_bindings.models.mdl44.import_mdl import create_armature +from SourceIO.blender_bindings.shared.model_container import ModelContainer +from SourceIO.blender_bindings.operators.import_settings_base import ModelOptions +from SourceIO.blender_bindings.utils.bpy_utils import add_material, is_blender_4_1, get_or_create_material +from SourceIO.blender_bindings.utils.fast_mesh import FastMesh +from SourceIO.library.models.mdl.structs.header import StudioHDRFlags +from SourceIO.library.models.mdl.v44.vertex_animation_cache import preprocess_vertex_animation +from SourceIO.library.models.mdl.v49.flex_expressions import * +from SourceIO.library.models.mdl.v49.mdl_file import MdlV49 +from SourceIO.library.models.vtx.v7.vtx import Vtx +from SourceIO.library.models.vvd import Vvd +from SourceIO.library.shared.content_manager import ContentManager +from SourceIO.library.shared.content_manager.provider import ContentProvider +from SourceIO.library.utils.common import get_slice +from SourceIO.library.utils.path_utilities import path_stem, collect_full_material_names +from SourceIO.library.utils.perf_sampler import timed +from SourceIO.logger import SourceLogMan + +log_manager = SourceLogMan() +logger = log_manager.get_logger('Source1::ModelLoader') + + + +def import_model(content_manager: ContentManager, mdl: MdlV49, vtx: Vtx, vvd: Vvd, + options: ModelOptions): + full_material_names = collect_full_material_names([mat.name for mat in mdl.materials], mdl.materials_paths, content_manager) + [setattr(mat, 'bpy_material', get_or_create_material(mat.name, full_material_names[mat.name])) for mat in mdl.materials if mat.bpy_material is None] + # ensure all MaterialV49 has its bpy_material counterpart + + objects = [] + bodygroups = defaultdict(list) + attachments = [] + desired_lod = 0 + all_vertices = vvd.lod_data[desired_lod] + extra_stuff = [] + static_prop = mdl.header.flags & StudioHDRFlags.STATIC_PROP != 0 + vert_anim_fixed_point_scale = mdl.header.vert_anim_fixed_point_scale if (mdl.header.flags & StudioHDRFlags.VERT_ANIM_FIXED_POINT_SCALE !=0 ) else 1/4096 + armature = None + vertex_anim_cache = preprocess_vertex_animation(mdl, vvd) + + scale = options.scale + create_drivers = options.create_flex_drivers + debug_stereo_balance = options.debug_stereo_balance + load_refpose = options.load_refpose + + if not static_prop: + armature = create_armature(mdl, scale, load_refpose) + + for vtx_body_part, body_part in zip(vtx.body_parts, mdl.body_parts): + for vtx_model, model in zip(vtx_body_part.models, body_part.models): + + if model.vertex_count == 0: + continue + object_name = model.name + mesh_name = f'{mdl.header.name}_{body_part.name}_{object_name}_MESH' + mesh_data = FastMesh.new(mesh_name) + mesh_obj = bpy.data.objects.new(object_name, mesh_data) + + mesh_obj['active_skin'] = '0' + mesh_obj['model_type'] = 's1' + default_skin_groups = {str(n): list(map(lambda a: a.name, group)) for (n, group) in enumerate(mdl.skin_groups)} + + objects.append(mesh_obj) + bodygroups[body_part.name].append(mesh_obj) + mesh_obj['prop_path'] = path_stem(mdl.header.name) + + model_vertices = get_slice(all_vertices, model.vertex_offset, model.vertex_count) + vtx_vertices, indices_array, material_indices_array = merge_meshes(model, vtx_model.model_lods[desired_lod]) + + indices_array = np.array(indices_array, dtype=np.uint32) + vertices = model_vertices[vtx_vertices] + vertices_vertex = vertices['vertex'] + + mesh_data.from_pydata(vertices_vertex * scale, [], np.flip(indices_array).reshape((-1, 3))) + mesh_data.update() + + mesh_data.polygons.foreach_set("use_smooth", np.ones(len(mesh_data.polygons), np.uint32)) + mesh_data.normals_split_custom_set_from_vertices(vertices['normal']) + if not is_blender_4_1(): + mesh_data.use_auto_smooth = True + + material_remapper = np.zeros((material_indices_array.max() + 1,), dtype=np.uint32) + for mat_id in np.unique(material_indices_array): + mat_name = mdl.materials[mat_id].name + material = get_or_create_material(mat_name, full_material_names[mat_name]) + mdl.materials[mat_id].bpy_material = material + material_remapper[mat_id] = add_material(material, mesh_obj) + + skin_groups = {str(n): list(map(lambda a: a.bpy_material, group)) for (n, group) in enumerate(mdl.skin_groups)} + try: + mesh_obj['skin_groups'] = skin_groups + except: + mesh_obj['skin_groups'] = default_skin_groups + + mesh_data.polygons.foreach_set('material_index', material_remapper[material_indices_array[::-1]]) + + vertex_indices = np.zeros((len(mesh_data.loops, )), dtype=np.uint32) + mesh_data.loops.foreach_get('vertex_index', vertex_indices) + + uv_data = mesh_data.uv_layers.new() + uvs = vertices['uv'] + uvs[:, 1] = 1 - uvs[:, 1] + uv_data.data.foreach_set('uv', uvs[vertex_indices].flatten()) + + if vvd.extra_data: + for extra_type, extra_data in vvd.extra_data.items(): + extra_data = extra_data.reshape((-1, 2)) + extra_uv = get_slice(extra_data, model.vertex_offset, model.vertex_count) + extra_uv = extra_uv[vtx_vertices] + uv_data = mesh_data.uv_layers.new(name=extra_type.name) + extra_uv[:, 1] = 1 - extra_uv[:, 1] + uv_data.data.foreach_set('uv', extra_uv[vertex_indices].flatten()) + + if not static_prop: + modifier = mesh_obj.modifiers.new( + type="ARMATURE", name="Armature") + modifier.object = armature + mesh_obj.parent = armature + + weight_groups = {bone.name: mesh_obj.vertex_groups.new(name=bone.name) for bone in mdl.bones} + + for n, (bone_indices, bone_weights) in enumerate(zip(vertices['bone_id'], vertices['weight'])): + for bone_index, weight in zip(bone_indices, bone_weights): + if weight > 0: + bone_name = mdl.bones[bone_index].name + weight_groups[bone_name].add([n], weight, 'REPLACE') + + flexes = [] + for mesh in model.meshes: + if mesh.flexes: + flexes.extend([(mdl.flex_names[flex.flex_desc_index], flex) for flex in mesh.flexes]) + + if flexes: + mesh_obj.shape_key_add(name='base') + + if debug_stereo_balance: + # debug tool to get stereo flex balances. i'll leave it here just in case + side_right = mesh_obj.vertex_groups.new(name='blendright') + side_left = mesh_obj.vertex_groups.new(name='blendleft') + side_all = np.zeros(model.vertex_count, dtype=np.float32) + + for flex_name, flex_desc in flexes: + vertex_animation = vertex_anim_cache[flex_name] + side = get_slice(vertex_animation['side'], model.vertex_offset, model.vertex_count).ravel() + side_all = np.maximum(side_all, side) + side_all = side_all[vtx_vertices] + 0.0 + + for n, vert in enumerate(vtx_vertices): + side_right.add([n], side_all[n], 'REPLACE') + side_left.add([n], 1-side_all[n], 'REPLACE') + + for flex_name, flex_desc in flexes: + vertex_animation = vertex_anim_cache[flex_name] + flex_delta = get_slice(vertex_animation["pos"], model.vertex_offset, model.vertex_count) + flex_delta = flex_delta[vtx_vertices] * scale + model_vertices = get_slice(all_vertices['vertex'], model.vertex_offset, model.vertex_count) + model_vertices = model_vertices[vtx_vertices] * scale + + side = get_slice(vertex_animation["side"], model.vertex_offset, model.vertex_count) + side = side[vtx_vertices] + 0.0 + wrinkle = get_slice(vertex_animation["wrinkle"], model.vertex_offset, model.vertex_count) + wrinkle = wrinkle[vtx_vertices] + 0.0 # this will have to be explained to me :P + # model.vertex_count and vtx_vertices can differ in size, so doing something like this just makes it work? + # apparently vtx_vertices has duplicate indicies, which can be observed by turning it into a set. + # i'm just following here + # -hisanimations + + if flex_desc.partner_index: + partner_name = mdl.flex_names[flex_desc.partner_index] + flexes, sides = [flex_name, partner_name], [1-side, side] if not debug_stereo_balance else [1.0, 1.0] + else: + flexes, sides = [flex_name], [1.0] + + for n, (flex_name, side) in enumerate(zip(flexes, sides)): + shape_key = mesh_data.shape_keys.key_blocks.get(flex_name, None) or mesh_obj.shape_key_add( + name=flex_name) + shape_key.data.foreach_set("co", (flex_delta*side + model_vertices).ravel()) + shape_key.value = 0.0 + if flex_desc.partner_index and debug_stereo_balance: + if n == 0: shape_key.vertex_group = side_left.name + else: shape_key.vertex_group = side_right.name + + if flex_desc.vertex_anim_type == 1: + # establish it as an attribute for wrinkle maps and designate it as a stretch or compress map + if wrinkle.max() > 0: + wrinkle_name = f'WR.{flex_name}.S' + elif wrinkle.min() < 0: + wrinkle_name = f'WR.{flex_name}.C' + else: + continue + attr = mesh_data.attributes.get(wrinkle_name, None) or mesh_data.attributes.new(wrinkle_name, 'FLOAT', 'POINT') + wrinkle_data = (abs(wrinkle) * vert_anim_fixed_point_scale) * side + attr.data.foreach_set('value', wrinkle_data.ravel()) + + + if create_drivers: + create_flex_drivers(mesh_obj, mdl) + if options.generate_wrinkle_map_node_group: + generate_wrinkle_map_node_group(mesh_obj) + + mesh_data.validate() + + if model.has_eyeballs: + create_eyeballs(mdl, armature, mesh_obj, model, scale, extra_stuff) + + if mdl.attachments: + attachments = create_attachments(mdl, armature if not static_prop else objects[0], scale) + attachments.extend(extra_stuff) + + return ModelContainer(objects, bodygroups, [], attachments, armature, None) + + +def create_flex_drivers(obj, mdl: MdlV49): + from ...operators.flex_operators import SourceIO_PG_FlexController + if not obj.data.shape_keys: + return + all_exprs = mdl.rebuild_flex_rules() + data = obj.data + shape_key_block = data.shape_keys + + def _parse_simple_flex(missing_flex_name: str): + flexes = missing_flex_name.split('_') + if not all(flex in data.flex_controllers for flex in flexes): + return None + return Combo([FetchController(flex) for flex in flexes]), [(flex, 'fetch2') for flex in flexes] + + st = '\n ' + + for flex_controller_ui in mdl.flex_ui_controllers: + cont: SourceIO_PG_FlexController = data.flex_controllers.add() + + if flex_controller_ui.nway_controller: + nway_cont: SourceIO_PG_FlexController = data.flex_controllers.add() + nway_cont.stereo = False + multi_controller = next(filter(lambda a: a.name == flex_controller_ui.nway_controller, mdl.flex_controllers) + ) + nway_cont.name = flex_controller_ui.nway_controller + nway_cont.set_from_controller(multi_controller) + + if flex_controller_ui.stereo: + left_controller = next( + filter(lambda a: a.name == flex_controller_ui.left_controller, mdl.flex_controllers) + ) + right_controller = next( + filter(lambda a: a.name == flex_controller_ui.right_controller, mdl.flex_controllers) + ) + cont.stereo = True + cont.name = flex_controller_ui.name + assert left_controller.max == right_controller.max + assert left_controller.min == right_controller.min + cont.set_from_controller(left_controller) + else: + controller = next(filter(lambda a: a.name == flex_controller_ui.controller, mdl.flex_controllers)) + cont.stereo = False + cont.name = flex_controller_ui.name + cont.set_from_controller(controller) + blender_py_file = """ +import bpy + +def rclamped(val, a, b, c, d): + if ( a == b ): + return d if val >= b else c; + return c + (d - c) * min(max((val - a) / (b - a), 0.0), 1.0) + +def clamp(val, a, b): + return min(max(val, a), b) + +def nway(multi_value, flex_value, x, y, z, w): + if multi_value <= x or multi_value >= w: # outside of boundaries + multi_value = 0.0 + elif multi_value <= y: + multi_value = rclamped(multi_value, x, y, 0.0, 1.0) + elif multi_value >= z: + multi_value = rclamped(multi_value, z, w, 1.0, 0.0) + else: + multi_value = 1.0 + return multi_value * flex_value + +def nway(multi_value, flex_value, x, y, zw): + z=zw[0] + w=list(zw[1])[0] + if multi_value <= x or multi_value >= w: # outside of boundaries + multi_value = 0.0 + elif multi_value <= y: + multi_value = rclamped(multi_value, x, y, 0.0, 1.0) + elif multi_value >= z: + multi_value = rclamped(multi_value, z, w, 1.0, 0.0) + else: + multi_value = 1.0 + return multi_value * flex_value + + +def combo(*values): + val = values[0] + for v in values[1:]: + val*=v + return val + +def dom(dm, *values): + val = 1 + for v in values: + val *= v + return val * (1 - dm) + +def lower_eyelid_case(eyes_up_down,close_lid_v,close_lid): + if eyes_up_down > 0.0: + return (1.0 - eyes_up_down) * (1.0 - close_lid_v) * close_lid + else: + return (1.0 - close_lid_v) * close_lid + +def upper_eyelid_case(eyes_up_down,close_lid_v,close_lid): + if eyes_up_down < 0.0: + return (1.0 - abs(eyes_up_down)) * close_lid_v * close_lid + else: + return close_lid_v * close_lid + + +bpy.app.driver_namespace["combo"] = combo +bpy.app.driver_namespace["dom"] = dom +bpy.app.driver_namespace["nway"] = nway +bpy.app.driver_namespace["rclamped"] = rclamped + + """ + + run_function=""" +def create_drivers(): +""" + + def normalize_name(name): + return name.replace("-", "_").replace(" ", "_") + + for flex_name, (expr, inputs) in all_exprs.items(): + normalized_flex_name = normalize_name(flex_name) + driver_name = f"{normalized_flex_name}_driver" + if driver_name in globals(): + continue + + input_definitions = [] + for inp in inputs: + input_name = inp[0] + normalized_input_name = normalize_name(inp[0]) + if inp[1] in ('fetch1', '2WAY1', '2WAY0', 'NWAY', 'DUE'): + if 'left_' in input_name: + if 'Lid' in input_name: + input_definitions.append( + f'{normalized_input_name} = obj_data.flex_controllers["{input_name.replace("left_", "")}"].value_left') + else: + input_definitions.append( + f'{normalized_input_name.replace("left_", "")} = obj_data.flex_controllers["{input_name.replace("left_", "")}"].value_left') + elif 'right_' in input_name: + if 'Lid' in input_name: + input_definitions.append( + f'{normalized_input_name} = obj_data.flex_controllers["{input_name.replace("right_", "")}"].value_right') + else: + input_definitions.append( + f'{normalized_input_name.replace("right_", "")} = obj_data.flex_controllers["{input_name.replace("right_", "")}"].value_right') + else: + input_definitions.append( + f'{normalized_input_name} = obj_data.flex_controllers["{inp[0]}"].value') + elif inp[1] == 'fetch2': + input_definitions.append( + f'{normalized_input_name} = obj_data.shape_keys.key_blocks["{input_name}"].value') + else: + raise NotImplementedError(f'"{inp[1]}" is not supported') + print(f"{flex_name} = {expr}") + template_function = f""" +def {driver_name}(obj_data): + {st.join(input_definitions)} + return {expr} +""" + run_function += f""" + bpy.app.driver_namespace["{driver_name}"] = {driver_name} +""" + blender_py_file += template_function + + for shape_key in shape_key_block.key_blocks: + + flex_name = shape_key.name + normalized_flex_name = normalize_name(flex_name) + + if flex_name == 'base': + continue + if flex_name not in all_exprs: + warnings.warn(f'Rule for {flex_name} not found! Generating basic rule.') + expr, inputs = _parse_simple_flex(flex_name) or (None, None) + if not expr or not inputs: + warnings.warn(f'Failed to generate basic rule for {flex_name}!') + cont: SourceIO_PG_FlexController = data.flex_controllers.add() + cont.name = flex_name + cont.mode = 1 + cont.value_min = 0 + cont.value_max = 1 + template_function = f""" +def {normalized_flex_name}_driver(obj_data): + return obj_data.flex_controllers["{flex_name}"].value +""" + blender_py_file += template_function + else: + template_function = f""" +def {normalized_flex_name}_driver(obj_data): + {st.join([i[0] for i in inputs])} + return {expr} +""" + blender_py_file += template_function + run_function+=f""" + bpy.app.driver_namespace["{normalized_flex_name}_driver"] = {normalized_flex_name}_driver +""" + blender_py_file+=run_function + blender_py_file+=""" +create_drivers() +""" + + driver_file = bpy.data.texts.new(f'{mdl.header.name}.py') + driver_file.write(blender_py_file) + driver_file.use_module = True + driver_file.as_module().create_drivers() + + for shape_key in shape_key_block.key_blocks: + flex_name = shape_key.name + normalized_flex_name = normalize_name(flex_name) + + if flex_name == 'base': + continue + + shape_key.driver_remove("value") + fcurve = shape_key.driver_add("value") + [fcurve.modifiers.remove(mod) for mod in fcurve.modifiers] + + driver = fcurve.driver + driver.type = 'SCRIPTED' + driver.expression = f"{normalized_flex_name}_driver(obj_data)" + var = driver.variables.new() + var.name = 'obj_data' + var.targets[0].id_type = 'OBJECT' + var.targets[0].id = obj + var.targets[0].data_path = f"data" + + +def create_attachments(mdl: MdlV49, armature: bpy.types.Object, scale): + attachments = [] + for attachment in mdl.attachments: + empty = bpy.data.objects.new(attachment.name, None) + pos = Vector(attachment.pos) * scale + rot = Euler(attachment.rot) + + empty.matrix_basis.identity() + empty.scale *= scale + empty.location = pos + empty.rotation_euler = rot + + if armature.type == 'ARMATURE': + modifier = empty.constraints.new(type="CHILD_OF") + modifier.target = armature + modifier.subtarget = mdl.bones[attachment.parent_bone].name + modifier.inverse_matrix.identity() + + attachments.append(empty) + + return attachments + + +def import_animations(cm: ContentProvider, mdl: MdlV49, armature: bpy.types.Object, + scale: float): + bpy.ops.object.select_all(action="DESELECT") + bpy.context.scene.collection.objects.link(armature) + armature.select_set(True) + bpy.context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='POSE') + if not armature.animation_data: + armature.animation_data_create() + + for n, anim in enumerate(mdl.anim_descs): + animation_data = mdl.animations[n] + action = bpy.data.actions.new(anim.name) + action.use_fake_user = True + armature.animation_data.action = action + curve_per_bone = {} + + for bone in mdl.bones: + bone_name = bone.name + bl_bone = armature.pose.bones.get(bone_name) + bl_bone.rotation_mode = 'QUATERNION' + bone_string = f'pose.bones["{bone_name}"].' + group = action.groups.new(name=bone_name) + pos_curves = [] + rot_curves = [] + for i in range(3): + pos_curve = action.fcurves.new(data_path=bone_string + "location", index=i) + pos_curve.keyframe_points.add(anim.frame_count) + pos_curve.auto_smoothing = "CONT_ACCEL" + pos_curves.append(pos_curve) + pos_curve.group = group + for i in range(4): + rot_curve = action.fcurves.new(data_path=bone_string + "rotation_quaternion", index=i) + rot_curve.keyframe_points.add(anim.frame_count) + rot_curve.auto_smoothing = "CONT_ACCEL" + rot_curves.append(rot_curve) + rot_curve.group = group + curve_per_bone[bone_name] = pos_curves, rot_curves + for bone_id, bone in enumerate(mdl.bones): + for frame_id in range(anim.frame_count): + bl_bone = armature.pose.bones.get(bone.name) + pos_curves, rot_curves = curve_per_bone[bone.name] + anim_data = animation_data[frame_id, bone_id] + bl_bone.matrix_basis.identity() + pos = Vector(anim_data["pos"]) * scale + x, y, z, w = anim_data["rot"] + rot = Quaternion((w, x, y, z)) + mat = Matrix.Translation(pos) @ rot.to_matrix().to_4x4() + + if bl_bone.parent: + + mat = bl_bone.parent.matrix @ mat if bl_bone.parent else mat + bl_bone.matrix = mat + pos, rot = bl_bone.location, bl_bone.rotation_quaternion + for i in range(3): + pos_curves[i].keyframe_points[frame_id].co = (frame_id, (pos[i])) + + for i in range(4): + rot_curves[i].keyframe_points[frame_id].co = (frame_id, (rot[i])) + else: + mat = bl_bone.matrix.inverted() @ mat + pos, rot, scl = mat.decompose() + for i in range(3): + pos_curves[i].keyframe_points[frame_id].co = (frame_id, (pos[i])) + + for i in range(4): + rot_curves[i].keyframe_points[frame_id].co = (frame_id, (rot[i])) + bl_bone.matrix = Matrix.Identity(4) + for pos_curves, rot_curves in curve_per_bone.values(): + for curve in rot_curves + pos_curves: + curve.update() + bpy.ops.object.mode_set(mode='OBJECT') + bpy.context.scene.collection.objects.unlink(armature) diff --git a/blender_bindings/models/mdl49/import_mdl.py b/blender_bindings/models/mdl49/import_mdl.py index 019656c4..e053e4cd 100644 --- a/blender_bindings/models/mdl49/import_mdl.py +++ b/blender_bindings/models/mdl49/import_mdl.py @@ -6,9 +6,10 @@ from mathutils import Euler, Matrix, Quaternion, Vector from math import atan -from SourceIO.blender_bindings.models.common import merge_meshes, create_eyeballs +from SourceIO.blender_bindings.models.common import merge_meshes, create_eyeballs, generate_wrinkle_map_node_group, make_bodygroup_selectors from SourceIO.blender_bindings.models.mdl44.import_mdl import create_armature from SourceIO.blender_bindings.shared.model_container import ModelContainer +from SourceIO.blender_bindings.operators.import_settings_base import ModelOptions from SourceIO.blender_bindings.utils.bpy_utils import add_material, is_blender_4_1, get_or_create_material from SourceIO.blender_bindings.utils.fast_mesh import FastMesh from SourceIO.library.models.mdl.structs.header import StudioHDRFlags @@ -23,6 +24,7 @@ from SourceIO.library.utils.path_utilities import path_stem, collect_full_material_names from SourceIO.library.utils.perf_sampler import timed from SourceIO.logger import SourceLogMan +from string import ascii_lowercase log_manager = SourceLogMan() logger = log_manager.get_logger('Source1::ModelLoader') @@ -30,11 +32,11 @@ def import_model(content_manager: ContentManager, mdl: MdlV49, vtx: Vtx, vvd: Vvd, - scale=1.0, create_drivers=False, load_refpose=False): + options: ModelOptions): full_material_names = collect_full_material_names([mat.name for mat in mdl.materials], mdl.materials_paths, content_manager) [setattr(mat, 'bpy_material', get_or_create_material(mat.name, full_material_names[mat.name])) for mat in mdl.materials if mat.bpy_material is None] + skin_groups = {str(n): list(map(lambda a: a.bpy_material, group)) for (n, group) in enumerate(mdl.skin_groups)} # ensure all MaterialV49 has its bpy_material counterpart - objects = [] bodygroups = defaultdict(list) attachments = [] @@ -42,8 +44,15 @@ def import_model(content_manager: ContentManager, mdl: MdlV49, vtx: Vtx, vvd: Vv all_vertices = vvd.lod_data[desired_lod] extra_stuff = [] static_prop = mdl.header.flags & StudioHDRFlags.STATIC_PROP != 0 + vert_anim_fixed_point_scale = mdl.header.vert_anim_fixed_point_scale if (mdl.header.flags & StudioHDRFlags.VERT_ANIM_FIXED_POINT_SCALE !=0 ) else 1/4096 armature = None vertex_anim_cache = preprocess_vertex_animation(mdl, vvd) + + scale = options.scale + create_drivers = options.create_flex_drivers + debug_stereo_balance = options.debug_stereo_balance + load_refpose = options.load_refpose + if not static_prop: armature = create_armature(mdl, scale, load_refpose) @@ -51,6 +60,7 @@ def import_model(content_manager: ContentManager, mdl: MdlV49, vtx: Vtx, vvd: Vv for vtx_model, model in zip(vtx_body_part.models, body_part.models): if model.vertex_count == 0: + bodygroups[body_part.name].append(None) continue object_name = model.name mesh_name = f'{mdl.header.name}_{body_part.name}_{object_name}_MESH' @@ -87,7 +97,6 @@ def import_model(content_manager: ContentManager, mdl: MdlV49, vtx: Vtx, vvd: Vv mdl.materials[mat_id].bpy_material = material material_remapper[mat_id] = add_material(material, mesh_obj) - skin_groups = {str(n): list(map(lambda a: a.bpy_material, group)) for (n, group) in enumerate(mdl.skin_groups)} try: mesh_obj['skin_groups'] = skin_groups except: @@ -133,6 +142,23 @@ def import_model(content_manager: ContentManager, mdl: MdlV49, vtx: Vtx, vvd: Vv if flexes: mesh_obj.shape_key_add(name='base') + + if debug_stereo_balance: + # debug tool to get stereo flex balances. i'll leave it here just in case + side_right = mesh_obj.vertex_groups.new(name='blendright') + side_left = mesh_obj.vertex_groups.new(name='blendleft') + side_all = np.zeros(model.vertex_count, dtype=np.float32) + + for flex_name, flex_desc in flexes: + vertex_animation = vertex_anim_cache[flex_name] + side = get_slice(vertex_animation['side'], model.vertex_offset, model.vertex_count).ravel() + side_all = np.maximum(side_all, side) + side_all = side_all[vtx_vertices] + 0.0 + + for n, vert in enumerate(vtx_vertices): + side_right.add([n], side_all[n], 'REPLACE') + side_left.add([n], 1-side_all[n], 'REPLACE') + for flex_name, flex_desc in flexes: vertex_animation = vertex_anim_cache[flex_name] flex_delta = get_slice(vertex_animation["pos"], model.vertex_offset, model.vertex_count) @@ -140,39 +166,57 @@ def import_model(content_manager: ContentManager, mdl: MdlV49, vtx: Vtx, vvd: Vv model_vertices = get_slice(all_vertices['vertex'], model.vertex_offset, model.vertex_count) model_vertices = model_vertices[vtx_vertices] * scale - if create_drivers and flex_desc.partner_index: - partner_name = mdl.flex_names[flex_desc.partner_index] - partner_shape_key = (mesh_data.shape_keys.key_blocks.get(partner_name, None) or - mesh_obj.shape_key_add(name=partner_name)) - partner_shape_key.value = 0.0 - shape_key = (mesh_data.shape_keys.key_blocks.get(flex_name, None) or - mesh_obj.shape_key_add(name=flex_name)) - shape_key.value = 0.0 - - balance = model_vertices[:, 0] - balance_width = (model_vertices.max() - model_vertices.min()) * (1 - (99.3 / 100)) - balance = np.clip((-balance / balance_width / 2) + 0.5, 0, 1) - - flex_vertices = (flex_delta * balance[:, None]) + model_vertices - shape_key.data.foreach_set("co", flex_vertices.reshape(-1)) + side = get_slice(vertex_animation["side"], model.vertex_offset, model.vertex_count) + side = side[vtx_vertices] + 0.0 + wrinkle = get_slice(vertex_animation["wrinkle"], model.vertex_offset, model.vertex_count) + wrinkle = wrinkle[vtx_vertices] + 0.0 # this will have to be explained to me :P + # model.vertex_count and vtx_vertices can differ in size, so doing something like this just makes it work? + # apparently vtx_vertices has duplicate indicies, which can be observed by turning it into a set. + # i'm just following here + # -hisanimations - p_balance = 1 - balance - p_flex_vertices = (flex_delta * p_balance[:, None]) + model_vertices - partner_shape_key.data.foreach_set("co", p_flex_vertices.reshape(-1)) + if flex_desc.partner_index: + partner_name = mdl.flex_names[flex_desc.partner_index] + flexes, sides = [flex_name, partner_name], [1-side, side] if not debug_stereo_balance else [1.0, 1.0] else: + flexes, sides = [flex_name], [1.0] + + for n, (flex_name, side) in enumerate(zip(flexes, sides)): shape_key = mesh_data.shape_keys.key_blocks.get(flex_name, None) or mesh_obj.shape_key_add( name=flex_name) + shape_key.data.foreach_set("co", (flex_delta*side + model_vertices).ravel()) shape_key.value = 0.0 - shape_key.data.foreach_set("co", (flex_delta + model_vertices).ravel()) + if flex_desc.partner_index and debug_stereo_balance: + if n == 0: shape_key.vertex_group = side_left.name + else: shape_key.vertex_group = side_right.name + + if flex_desc.vertex_anim_type == 1: + # establish it as an attribute for wrinkle maps and designate it as a stretch or compress map + if wrinkle.max() > 0: + wrinkle_name = f'WR.{flex_name}.S' + elif wrinkle.min() < 0: + wrinkle_name = f'WR.{flex_name}.C' + else: + continue + attr = mesh_data.attributes.get(wrinkle_name, None) or mesh_data.attributes.new(wrinkle_name, 'FLOAT', 'POINT') + wrinkle_data = (abs(wrinkle) * vert_anim_fixed_point_scale) * side + attr.data.foreach_set('value', wrinkle_data.ravel()) + + if create_drivers: create_flex_drivers(mesh_obj, mdl) - mesh_data.validate() + if options.generate_wrinkle_map_node_group: + generate_wrinkle_map_node_group(mesh_obj) + + mesh_data.validate() if model.has_eyeballs: create_eyeballs(mdl, armature, mesh_obj, model, scale, extra_stuff) if mdl.attachments: attachments = create_attachments(mdl, armature if not static_prop else objects[0], scale) + if not static_prop: + make_bodygroup_selectors(mdl, armature, bodygroups) attachments.extend(extra_stuff) return ModelContainer(objects, bodygroups, [], attachments, armature, None) @@ -180,11 +224,284 @@ def import_model(content_manager: ContentManager, mdl: MdlV49, vtx: Vtx, vvd: Vv def create_flex_drivers(obj, mdl: MdlV49): from ...operators.flex_operators import SourceIO_PG_FlexController + from SourceIO.library.models.mdl.structs.flex import FlexController, FlexControllerUI, FlexOpType, FlexRule if not obj.data.shape_keys: return + + nway_expr = 'max(min(({0}-{1})/({2}-{1}),({4}-{0})/({4}-{3})),0)' + two_way_0_expr = 'clamp({}*-1)' + two_way_1_expr = 'clamp({})' + upper_eye_expr = '(1-abs(min({}, 0)))*{}*{}' + lower_eye_expr = '(1-abs(max({}, 0)))*(1-{})*{}' + + def number_format(x): + return ''.join((map(lambda a: ascii_lowercase[int(a)], f'{x:03d}'))) + '_' + all_exprs = mdl.rebuild_flex_rules() - data = obj.data - shape_key_block = data.shape_keys + bpy.types.Scene.t = all_exprs + data: bpy.types.Mesh = obj.data + shape_keys = data.shape_keys + kb = shape_keys.key_blocks + + def make_custom_property(name, min, max, default=0.0): + obj.data[name] = default + prop = obj.data.id_properties_ui(name) + prop.update(min=min, max=max) + + #tally = iter(range(999)) + def tally(): + for i in range(999): + yield ''.join(map(lambda a: ascii_lowercase[int(a)], f'{i:03d}')) + tally = tally() + + flexcontrollers = dict() + flexmap = dict() + + flexmap['flex_scale'] = flex_sort = f'{next(tally)}_fs' + flex = dict() + flex['controller'] = 'flex_scale' + flex['type'] = 0b00 + make_custom_property(flex_sort, -10, 10, 1.0) + + flexcontrollers['Flex Scale'] = flex + + for flex_controller_ui in mdl.flex_ui_controllers: + flex = dict() + + if flex_controller_ui.stereo: + flex['type'] = 0b01 + left_controller = next( + filter(lambda a: a.name == flex_controller_ui.left_controller, mdl.flex_controllers) + ) + right_controller = next( + filter(lambda a: a.name == flex_controller_ui.right_controller, mdl.flex_controllers) + ) + flexmap[left_controller.name] = left_sort = f'{next(tally)}_{left_controller.name}' + flexmap[right_controller.name] = right_sort = f'{next(tally)}_{right_controller.name}' + flex['left'] = left_controller.name + flex['right'] = right_controller.name + make_custom_property(left_sort, left_controller.min, left_controller.max) + make_custom_property(right_sort, right_controller.min, right_controller.max) + else: + flex['type'] = 0b00 + controller = next(filter(lambda a: a.name == flex_controller_ui.controller, mdl.flex_controllers)) + flexmap[controller.name] = controller_sort = f'{next(tally)}_{controller.name}' + flex['controller'] = controller.name + make_custom_property(controller_sort, controller.min, controller.max) + + if flex_controller_ui.nway_controller: + flex['type'] += 0b10 + nway = next(filter(lambda a: a.name == flex_controller_ui.nway_controller, mdl.flex_controllers)) + flexmap[nway.name] = nway_sort = f'{next(tally)}_{nway.name}' + flex['nway'] = nway.name + make_custom_property(nway_sort, nway.min, nway.max) + + flexcontrollers[flex_controller_ui.name] = flex + + obj.data['flexmap'] = flexmap + obj.data['flexcontrollers'] = flexcontrollers + + def var_tally(): + for i in range(999): + yield 'V'+str(i) + def dom_tally(): + for i in range(999): + yield 'D'+str(i) + + shape_keys = obj.data.shape_keys + + for name, (expr, inputs) in all_exprs.items(): + expr = expr.as_simple() + vtally = var_tally() + #dtally = dom_tally() + if kb.get(name): + kb[name].driver_remove('value') + driv = kb[name].driver_add('value') + else: + shape_keys[name] = 0.0 + driv = shape_keys.driver_add(f'["{name}"]') + [driv.modifiers.remove(mod) for mod in driv.modifiers] + driv = driv.driver + for input, type in set(inputs): + var = driv.variables.new() + var.name = next(vtally) + var.type = 'SINGLE_PROP' + targ = var.targets[0] + if type == 'fetch2': + if kb.get(input): + data_path = kb[input].path_from_id('value') + elif shape_keys.get(input): + data_path = f'["{input}"]' + else: + shape_keys[input] = 0.0 + data_path = f'["{input}"]' + + targ.id_type = 'KEY' + targ.id = shape_keys + targ.data_path = data_path + else: + targ.id_type = 'MESH' + targ.id = data + targ.data_path = f'["{flexmap[input]}"]' + + expr = expr.replace(input, var.name) + + + var = driv.variables.new() + var.name = 'FS' + var.type = 'SINGLE_PROP' + targ = var.targets[0] + targ.id_type = 'MESH' + targ.id = data + targ.data_path = '["{}"]'.format(flexmap['flex_scale']) + assert len(expr) < 256 + driv.expression = expr.replace('--', '+') + '*FS' + + return + int_to_flex = {n: flexmap[mdl.flex_controllers[n].name] for n, flex in enumerate(mdl.flex_controllers)} + + + for rule in mdl.flex_rules: + vtally = var_tally() + dtally = dom_tally() + expr = '' + stack = [] + + op_count = len(rule.flex_ops) + + for n, op in enumerate(rule.flex_ops): + last = (n + 1) == op_count + flex_op = op.op + if flex_op == FlexOpType.CONST: + stack.append(Value(op.value)) + elif flex_op == FlexOpType.FETCH1: + inputs.append((self.flex_controllers[op.value].name, 'fetch1')) + fc_ui = flex_controllers[self.flex_controllers[op.value].name] + stack.append(FetchController(fc_ui.name, fc_ui.stereo)) + elif flex_op == FlexOpType.FETCH2: + inputs.append((self.flex_names[op.value], 'fetch2')) + stack.append(FetchFlex(self.flex_names[op.value])) + elif flex_op == FlexOpType.ADD: + stack.append(Add(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.SUB: + stack.append(Sub(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.MUL: + stack.append(Mul(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.DIV: + stack.append(Div(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.NEG: + stack.append(Neg(stack.pop(-1))) + elif flex_op == FlexOpType.MAX: + stack.append(Max(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.MIN: + stack.append(Min(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.COMBO: + stack.append(Combo(*[stack.pop(-1) for _ in range(op.value)])) + elif flex_op == FlexOpType.DOMINATE: + stack.append(Dominator(*[stack.pop(-1) for _ in range(op.value + 1)])) + elif flex_op == FlexOpType.TWO_WAY_0: + inputs.append((self.flex_controllers[op.value].name, '2WAY0')) + fc_ui = flex_controllers[self.flex_controllers[op.value].name] + stack.append(RClamp(FetchController(fc_ui.name, fc_ui.stereo), + -1, 0, 1, 0)) + elif flex_op == FlexOpType.TWO_WAY_1: + inputs.append((self.flex_controllers[op.value].name, '2WAY1')) + fc_ui = flex_controllers[self.flex_controllers[op.value].name] + stack.append(Clamp(FetchController(fc_ui.name, fc_ui.stereo), 0, 1), ) + elif flex_op == FlexOpType.NWAY: + + inputs.append((self.flex_controllers[op.value].name, 'NWAY')) + fc_ui = flex_controllers[self.flex_controllers[op.value].name] + flex_cnt = FetchController(fc_ui.name, fc_ui.stereo) + + flex_cnt_value = int(stack.pop(-1).value) + inputs.append((self.flex_controllers[flex_cnt_value].name, 'NWAY')) + fc_ui = flex_controllers[self.flex_controllers[flex_cnt_value].name] + multi_cnt = FetchController(fc_ui.nway_controller, fc_ui.stereo) + + # Reversed the order, revert back if it wont help + f_w = stack.pop(-1) + f_z = stack.pop(-1) + f_y = stack.pop(-1) + f_x = stack.pop(-1) + final_expr = NWay(multi_cnt, flex_cnt, f_x, f_y, f_z, f_w) + stack.append(final_expr) + elif flex_op == FlexOpType.DME_UPPER_EYELID: + close_lid_v_controller = self.flex_controllers[op.value] + inputs.append((close_lid_v_controller.name, 'DUE')) + close_lid_v = RClamp(FetchController(close_lid_v_controller.name), + close_lid_v_controller.min, close_lid_v_controller.max, + 0, 1) + + flex_cnt_value = int(stack.pop(-1).value) + close_lid_controller = self.flex_controllers[flex_cnt_value] + inputs.append((close_lid_controller.name, 'DUE')) + close_lid = RClamp(FetchController(close_lid_controller.name), + close_lid_controller.min, close_lid_controller.max, + 0, 1) + + blink_index = int(stack.pop(-1).value) + # blink = Value(0.0) + # if blink_index >= 0: + # blink_controller = self.flex_controllers[blink_index] + # inputs.append((blink_controller.name, 'DUE')) + # blink_fetch = FetchController(blink_controller.name) + # blink = CustomFunction('rclamped', blink_fetch, + # blink_controller.min, blink_controller.max, + # 0, 1) + + eye_up_down_index = int(stack.pop(-1).value) + eye_up_down = Value(0.0) + if eye_up_down_index >= 0: + eye_up_down_controller = self.flex_controllers[eye_up_down_index] + inputs.append((eye_up_down_controller.name, 'DUE')) + eye_up_down_fetch = FetchController(eye_up_down_controller.name) + eye_up_down = RClamp(eye_up_down_fetch, + eye_up_down_controller.min, eye_up_down_controller.max, + -1, 1) + + stack.append(CustomFunction('upper_eyelid_case', eye_up_down, close_lid_v, close_lid)) + elif flex_op == FlexOpType.DME_LOWER_EYELID: + close_lid_v_controller = self.flex_controllers[op.value] + inputs.append((close_lid_v_controller.name, 'DUE')) + close_lid_v = RClamp(FetchController(close_lid_v_controller.name), + close_lid_v_controller.min, close_lid_v_controller.max, + 0, 1) + + flex_cnt_value = int(stack.pop(-1).value) + close_lid_controller = self.flex_controllers[flex_cnt_value] + inputs.append((close_lid_controller.name, 'DUE')) + close_lid = RClamp(FetchController(close_lid_controller.name), + close_lid_controller.min, close_lid_controller.max, + 0, 1) + + blink_index = int(stack.pop(-1).value) + # blink = Value(0.0) + # if blink_index >= 0: + # blink_controller = self.flex_controllers[blink_index] + # inputs.append((blink_controller.name, 'DUE')) + # blink_fetch = FetchController(blink_controller.name) + # blink = CustomFunction('rclamped', blink_fetch, + # blink_controller.min, blink_controller.max, + # 0, 1) + + eye_up_down_index = int(stack.pop(-1).value) + eye_up_down = Value(0.0) + if eye_up_down_index >= 0: + eye_up_down_controller = self.flex_controllers[eye_up_down_index] + inputs.append((eye_up_down_controller.name, 'DUE')) + eye_up_down_fetch = FetchController(eye_up_down_controller.name) + eye_up_down = RClamp(eye_up_down_fetch, + eye_up_down_controller.min, eye_up_down_controller.max, + -1, 1) + + stack.append(CustomFunction('lower_eyelid_case', eye_up_down, close_lid_v, close_lid)) + elif flex_op == FlexOpType.OPEN: + continue + else: + print("Unknown OP", op) + + return def _parse_simple_flex(missing_flex_name: str): flexes = missing_flex_name.split('_') @@ -277,8 +594,8 @@ def lower_eyelid_case(eyes_up_down,close_lid_v,close_lid): return (1.0 - close_lid_v) * close_lid def upper_eyelid_case(eyes_up_down,close_lid_v,close_lid): - if eyes_up_down > 0.0: - return (1.0 + eyes_up_down) * close_lid_v * close_lid + if eyes_up_down < 0.0: + return (1.0 - abs(eyes_up_down)) * close_lid_v * close_lid else: return close_lid_v * close_lid @@ -392,7 +709,7 @@ def {normalized_flex_name}_driver(obj_data): shape_key.driver_remove("value") fcurve = shape_key.driver_add("value") - fcurve.modifiers.remove(fcurve.modifiers[0]) + [fcurve.modifiers.remove(mod) for mod in fcurve.modifiers] driver = fcurve.driver driver.type = 'SCRIPTED' diff --git a/blender_bindings/models/mdl52/__init__.py b/blender_bindings/models/mdl52/__init__.py index 4c59a06d..58161a69 100644 --- a/blender_bindings/models/mdl52/__init__.py +++ b/blender_bindings/models/mdl52/__init__.py @@ -47,7 +47,7 @@ def import_mdl52(model_path: TinyPath, buffer: Buffer, import traceback traceback.print_exc() - container = import_model(content_manager, mdl, vtx, vvd, vvc, options.scale) + container = import_model(content_manager, mdl, vtx, vvd, vvc, options) if options.import_physics: phy_buffer = content_manager.find_file(model_path.with_suffix(".phy")) if phy_buffer is None: diff --git a/blender_bindings/models/mdl52/import_mdl.py b/blender_bindings/models/mdl52/import_mdl.py index 0ea62b06..b8e35d73 100644 --- a/blender_bindings/models/mdl52/import_mdl.py +++ b/blender_bindings/models/mdl52/import_mdl.py @@ -3,9 +3,10 @@ import bpy import numpy as np -from SourceIO.blender_bindings.models.common import merge_meshes, create_eyeballs +from SourceIO.blender_bindings.models.common import merge_meshes, create_eyeballs, generate_wrinkle_map_node_group from SourceIO.blender_bindings.models.mdl49.import_mdl import create_armature, create_attachments, create_flex_drivers from SourceIO.blender_bindings.shared.model_container import ModelContainer +from SourceIO.blender_bindings.operators.import_settings_base import ModelOptions from SourceIO.blender_bindings.utils.bpy_utils import add_material, is_blender_4_1, get_or_create_material from SourceIO.blender_bindings.utils.fast_mesh import FastMesh from SourceIO.library.models.mdl.structs.header import StudioHDRFlags @@ -24,7 +25,7 @@ def import_model(content_provider: ContentProvider, mdl: MdlV52, vtx: Vtx, vvd: Vvd, vvc: Vvc, - scale=1.0, create_drivers=False, load_refpose=False): + options: ModelOptions): full_material_names = collect_full_material_names([mat.name for mat in mdl.materials], mdl.materials_paths, content_provider) [setattr(mat, 'bpy_material', get_or_create_material(mat.name, full_material_names[mat.name])) for mat in mdl.materials if mat.bpy_material is None] @@ -39,8 +40,13 @@ def import_model(content_provider: ContentProvider, mdl: MdlV52, vtx: Vtx, vvd: vertex_anim_cache = preprocess_vertex_animation(mdl, vvd) static_prop = mdl.header.flags & StudioHDRFlags.STATIC_PROP != 0 + vert_anim_fixed_point_scale = mdl.header.vert_anim_fixed_point_scale if (mdl.header.flags & StudioHDRFlags.VERT_ANIM_FIXED_POINT_SCALE !=0 ) else 1/4096 armature = None + scale = options.scale + create_drivers = options.create_flex_drivers + debug_stereo_balance = options.debug_stereo_balance + if not static_prop: armature = create_armature(mdl, scale) @@ -131,44 +137,71 @@ def import_model(content_provider: ContentProvider, mdl: MdlV52, vtx: Vtx, vvd: if flexes: mesh_obj.shape_key_add(name='base') - for flex_name, flex_desc in flexes: - vertex_animation = vertex_anim_cache[flex_name] - flex_delta = get_slice(vertex_animation["pos"], model.vertex_offset, model.vertex_count) - flex_delta = flex_delta[vtx_vertices] * scale - model_vertices = get_slice(all_vertices['vertex'], model.vertex_offset, model.vertex_count) - model_vertices = model_vertices[vtx_vertices] * scale - - if create_drivers and flex_desc.partner_index: - partner_name = mdl.flex_names[flex_desc.partner_index] - partner_shape_key = (mesh_data.shape_keys.key_blocks.get(partner_name, None) or - mesh_obj.shape_key_add(name=partner_name)) - partner_shape_key.value = 0.0 - shape_key = (mesh_data.shape_keys.key_blocks.get(flex_name, None) or - mesh_obj.shape_key_add(name=flex_name)) - shape_key.value = 0.0 - - balance = model_vertices[:, 0] - balance_width = (model_vertices.max() - model_vertices.min()) * (1 - (99.3 / 100)) - balance = np.clip((-balance / balance_width / 2) + 0.5, 0, 1) - - flex_vertices = (flex_delta * balance[:, None]) + model_vertices - shape_key.data.foreach_set("co", flex_vertices.reshape(-1)) - - p_balance = 1 - balance - p_flex_vertices = (flex_delta * p_balance[:, None]) + model_vertices - partner_shape_key.data.foreach_set("co", p_flex_vertices.reshape(-1)) - else: - shape_key = mesh_data.shape_keys.key_blocks.get(flex_name, None) or mesh_obj.shape_key_add( - name=flex_name) - shape_key.value = 0.0 - - shape_key.data.foreach_set("co", (flex_delta + model_vertices).reshape(-1)) - if create_drivers: - create_flex_drivers(mesh_obj, mdl) + + if debug_stereo_balance: + # debug tool to get stereo flex balances. i'll leave it here just in case + side_right = mesh_obj.vertex_groups.new(name='blendright') + side_left = mesh_obj.vertex_groups.new(name='blendleft') + side_all = np.zeros(model.vertex_count, dtype=np.float32) + + for flex_name, flex_desc in flexes: + vertex_animation = vertex_anim_cache[flex_name] + side = get_slice(vertex_animation['side'], model.vertex_offset, model.vertex_count).ravel() + side_all = np.maximum(side_all, side) + side_all = side_all[vtx_vertices] + 0.0 + + for n, vert in enumerate(vtx_vertices): + side_right.add([n], side_all[n], 'REPLACE') + side_left.add([n], 1-side_all[n], 'REPLACE') + + for flex_name, flex_desc in flexes: + vertex_animation = vertex_anim_cache[flex_name] + flex_delta = get_slice(vertex_animation["pos"], model.vertex_offset, model.vertex_count) + flex_delta = flex_delta[vtx_vertices] * scale + + side = get_slice(vertex_animation["side"], model.vertex_offset, model.vertex_count) + side = side[vtx_vertices] + 0.0 + wrinkle = get_slice(vertex_animation["wrinkle"], model.vertex_offset, model.vertex_count) + wrinkle = wrinkle[vtx_vertices] + 0.0 # this will have to be explained to me :P + # model.vertex_count and vtx_vertices can differ in size, so doing something like this just makes it work? + # apparently vtx_vertices has duplicate indicies, which can be observed by turning it into a set. + # i'm just following here + # -hisanimations + + model_vertices = get_slice(all_vertices['vertex'], model.vertex_offset, model.vertex_count) + model_vertices = model_vertices[vtx_vertices] * scale + + if flex_desc.partner_index: + partner_name = mdl.flex_names[flex_desc.partner_index] + flexes, sides = [flex_name, partner_name], [1-side, side] if not debug_stereo_balance else [1.0, 1.0] + else: + flexes, sides = [flex_name], [1.0] + + for flex_name, side in zip(flexes, sides): + shape_key = mesh_data.shape_keys.key_blocks.get(flex_name, None) or mesh_obj.shape_key_add( + name=flex_name) + shape_key.data.foreach_set("co", (flex_delta*side + model_vertices).ravel()) + shape_key.value = 0.0 + + if flex_desc.vertex_anim_type == 1: + mesh_data: bpy.types.Mesh + if wrinkle.max() > 0: + wrinkle_name = f'WR.{flex_name}.S' + if wrinkle.min() < 0: + wrinkle_name = f'WR.{flex_name}.C' + + attr: bpy.types.Attribute = mesh_data.attributes.get(wrinkle_name, None) or mesh_data.attributes.new(wrinkle_name, 'FLOAT', 'POINT') + wrinkle_data = (abs(wrinkle) * vert_anim_fixed_point_scale) * side + attr.data.foreach_set('value', wrinkle_data.ravel()) + if create_drivers: + create_flex_drivers(mesh_obj, mdl) + if options.generate_wrinkle_map_node_group: + generate_wrinkle_map_node_group(mesh_obj) + mesh_data.validate() - if model.has_eyeballs: - create_eyeballs(mdl, armature, mesh_obj, model, scale, extra_stuff) + if model.has_eyeballs: + create_eyeballs(mdl, armature, mesh_obj, model, scale, extra_stuff) if mdl.attachments: attachments = create_attachments(mdl, armature if not static_prop else objects[0], scale) diff --git a/blender_bindings/operators/import_settings_base.py b/blender_bindings/operators/import_settings_base.py index 78f541ab..ca78e5b5 100644 --- a/blender_bindings/operators/import_settings_base.py +++ b/blender_bindings/operators/import_settings_base.py @@ -37,10 +37,12 @@ class ModelOptions(SharedOptions, Source1SharedSettings): load_refpose: BoolProperty(name="Load Ref pose", default=False, subtype='UNSIGNED') import_animations: BoolProperty(name="Load animations", default=False, subtype='UNSIGNED') - create_flex_drivers: BoolProperty(name="Create drivers for flexes", default=False, subtype='UNSIGNED') + create_flex_drivers: BoolProperty(name="Create drivers for flexes", default=True, subtype='UNSIGNED') bodygroup_grouping: BoolProperty(name="Group meshes by bodygroup", default=True, subtype='UNSIGNED') import_textures: BoolProperty(name="Import materials", default=True, subtype='UNSIGNED') + debug_stereo_balance: BoolProperty(name='Debug Stereo Flex Balance', description='Add vertex groups to show how stereo flexes blend. If enabled, the balance will not be mixed into shape keys.', default=False, subtype='UNSIGNED') + generate_wrinkle_map_node_group: BoolProperty(name='Generate Wrinkle Map Nodes', description='Generates a node group that stores wrinkle map information, which you can access in the Shader Editor with the "tension" attribute.', default=True, subtype='UNSIGNED') @classmethod def default(cls): diff --git a/library/models/mdl/structs/flex.py b/library/models/mdl/structs/flex.py index 1766f3c6..38d7bdb1 100644 --- a/library/models/mdl/structs/flex.py +++ b/library/models/mdl/structs/flex.py @@ -251,6 +251,6 @@ class VertAnimWrinkleV49(VertAnimV49): ('side', np.uint8, (1,)), ('vertex_delta', np.float16, (3,)), ('normal_delta', np.float16, (3,)), - ('wrinkle_delta', np.float16, (1,)), + ('wrinkle_delta', np.int16, (1,)), ] ) diff --git a/library/models/mdl/v44/mdl_file copy.py b/library/models/mdl/v44/mdl_file copy.py new file mode 100644 index 00000000..a3987389 --- /dev/null +++ b/library/models/mdl/v44/mdl_file copy.py @@ -0,0 +1,325 @@ +import traceback +from dataclasses import dataclass, field +from typing import Mapping + +import numpy.typing as npt + +from SourceIO.library.models.mdl import Mdl +from SourceIO.library.models.mdl.structs.attachment import Attachment +from SourceIO.library.models.mdl.structs.bodygroup import BodyPart +from SourceIO.library.models.mdl.structs.bone import Bone +from SourceIO.library.models.mdl.structs.flex import FlexController, FlexControllerUI, FlexOpType, FlexRule +from SourceIO.library.models.mdl.structs.header import MdlHeaderV44 +from SourceIO.library.models.mdl.structs.local_animation import StudioAnimDesc +from SourceIO.library.models.mdl.structs.material import MaterialV49 +from SourceIO.library.models.mdl.structs.sequence import StudioSequence +from SourceIO.library.models.mdl.v49.flex_expressions import * +from SourceIO.library.utils import Buffer +from SourceIO.library.utils.kv_parser import ValveKeyValueParser + + +class _AnimBlocks: + def __init__(self): + self.name = '' + self.blocks = [] + + +@dataclass(slots=True) +class MdlV44(Mdl): + header: MdlHeaderV44 + + bones: list[Bone] = field(repr=False) + skin_groups: list[list[str]] + materials: list[MaterialV49] + materials_paths: list[str] + + flex_names: list[str] = field(repr=False) + flex_controllers: list[FlexController] = field(repr=False) + flex_ui_controllers: list[FlexControllerUI] = field(repr=False) + flex_rules: list[FlexRule] = field(repr=False) + + body_parts: list[BodyPart] = field(repr=False) + + attachments: list[Attachment] = field(repr=False) + + anim_descs: list[StudioAnimDesc] = field(repr=False) + sequences: list[StudioSequence] = field(repr=False) + animations: list[npt.NDArray] = field(repr=False) + + key_values_raw: str + key_values: Mapping + + include_models: list[str] + + @classmethod + def from_buffer(cls, buffer: Buffer): + header = MdlHeaderV44.from_buffer(buffer) + + bones = [] + buffer.seek(header.bone_offset) + for bone_id in range(header.bone_count): + bone = Bone.from_buffer(buffer, header.version) + bone.bone_id = bone_id + bones.append(bone) + + materials = [] + buffer.seek(header.texture_offset) + for _ in range(header.texture_count): + texture = MaterialV49.from_buffer(buffer, header.version) + materials.append(texture) + + materials_paths = [] + buffer.seek(header.texture_path_offset) + for _ in range(header.texture_path_count): + materials_paths.append(buffer.read_source1_string(0)) + + skin_groups = [] + buffer.seek(header.skin_family_offset) + for _ in range(header.skin_family_count): + skin_group = [] + for _ in range(header.skin_reference_count): + texture_index = buffer.read_uint16() + skin_group.append(materials[texture_index]) + skin_groups.append(skin_group) + + diff_start = 0 + for skin_info in skin_groups[1:]: + for n, (a, b) in enumerate(zip(skin_groups[0], skin_info)): + if a == b: + diff_start = max(n, diff_start) + break + + for n, skin_info in enumerate(skin_groups): + skin_groups[n] = skin_info[:diff_start] + + flex_names = [] + buffer.seek(header.flex_desc_offset) + for _ in range(header.flex_desc_count): + flex_names.append(buffer.read_source1_string(buffer.tell())) + + flex_controllers = [] + buffer.seek(header.flex_controller_offset) + for _ in range(header.flex_controller_count): + controller = FlexController.from_buffer(buffer, header.version) + flex_controllers.append(controller) + + flex_rules = [] + buffer.seek(header.flex_rule_offset) + for _ in range(header.flex_rule_count): + rule = FlexRule.from_buffer(buffer, header.version) + flex_rules.append(rule) + + attachments = [] + buffer.seek(header.local_attachment_offset) + for _ in range(header.local_attachment_count): + attachment = Attachment.from_buffer(buffer, header.version) + attachments.append(attachment) + + flex_ui_controllers = [] + buffer.seek(header.flex_controller_ui_offset) + for _ in range(header.flex_controller_ui_count): + flex_controller = FlexControllerUI.from_buffer(buffer, header.version) + flex_ui_controllers.append(flex_controller) + + body_parts = [] + buffer.seek(header.body_part_offset) + for _ in range(header.body_part_count): + body_part = BodyPart.from_buffer(buffer, header.version) + body_parts.append(body_part) + + buffer.seek(header.key_value_offset) + key_values_raw = buffer.read(header.key_value_size).strip(b'\x00').decode('latin1') + if key_values_raw: + parser = ValveKeyValueParser(buffer_and_name=(key_values_raw, 'memory'), self_recover=True) + parser.parse() + key_values = parser.tree + else: + key_values = {} + + local_animations = [] + buffer.seek(header.local_animation_offset) + for _ in range(header.local_animation_count): + local_animations.append(StudioAnimDesc.from_buffer(buffer)) + + local_sequences = [] + buffer.seek(header.local_sequence_offset) + for _ in range(header.local_sequence_count): + local_sequences.append(StudioSequence.from_buffer(buffer, header.version)) + + animations = [] + for anim_desc in local_animations: + try: + animations.append(anim_desc.read_animations(buffer, bones)) + except (AssertionError, ValueError): + traceback.print_exc() + animations.extend([None] * (len(animations) - len(local_animations))) + break + + include_models = [] + buffer.seek(header.include_model_offset) + for inc_model in range(header.include_model_count): + entry = buffer.tell() + label = buffer.read_source1_string(entry) + path = buffer.read_source1_string(entry) + include_models.append(path) + + # for anim + return cls(header, bones, skin_groups, materials, materials_paths, flex_names, flex_controllers, + flex_ui_controllers, flex_rules, body_parts, attachments, local_animations, local_sequences, + animations, key_values_raw, key_values, include_models) + + def rebuild_flex_rules(self): + flex_controllers: dict[str, FlexControllerUI] = {f.left_controller: f for f in self.flex_ui_controllers if + f.stereo} + flex_controllers.update({f.right_controller: f for f in self.flex_ui_controllers if f.stereo}) + flex_controllers.update({f.nway_controller: f for f in self.flex_ui_controllers if f.nway_controller}) + flex_controllers.update({f.name: f for f in self.flex_ui_controllers}) + rules = {} + for rule in self.flex_rules: + stack = [] + inputs = [] + # if 1: + try: + for op in rule.flex_ops: + flex_op = op.op + if flex_op == FlexOpType.CONST: + stack.append(Value(op.value)) + elif flex_op == FlexOpType.FETCH1: + inputs.append((self.flex_controllers[op.value].name, 'fetch1')) + fc_ui = flex_controllers[self.flex_controllers[op.value].name] + stack.append(FetchController(fc_ui.name, fc_ui.stereo)) + elif flex_op == FlexOpType.FETCH2: + inputs.append((self.flex_names[op.value], 'fetch2')) + stack.append(FetchFlex(self.flex_names[op.value])) + elif flex_op == FlexOpType.ADD: + stack.append(Add(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.SUB: + stack.append(Sub(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.MUL: + stack.append(Mul(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.DIV: + stack.append(Div(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.NEG: + stack.append(Neg(stack.pop(-1))) + elif flex_op == FlexOpType.MAX: + stack.append(Max(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.MIN: + stack.append(Min(stack.pop(-1), stack.pop(-1))) + elif flex_op == FlexOpType.COMBO: + stack.append(Combo(*[stack.pop(-1) for _ in range(op.value)])) + elif flex_op == FlexOpType.DOMINATE: + stack.append(Dominator(*[stack.pop(-1) for _ in range(op.value + 1)])) + elif flex_op == FlexOpType.TWO_WAY_0: + inputs.append((self.flex_controllers[op.value].name, '2WAY0')) + fc_ui = flex_controllers[self.flex_controllers[op.value].name] + stack.append(RClamp(FetchController(fc_ui.name, fc_ui.stereo), + -1, 0, 1, 0)) + elif flex_op == FlexOpType.TWO_WAY_1: + inputs.append((self.flex_controllers[op.value].name, '2WAY1')) + fc_ui = flex_controllers[self.flex_controllers[op.value].name] + stack.append(Clamp(FetchController(fc_ui.name, fc_ui.stereo), 0, 1), ) + elif flex_op == FlexOpType.NWAY: + + inputs.append((self.flex_controllers[op.value].name, 'NWAY')) + fc_ui = flex_controllers[self.flex_controllers[op.value].name] + flex_cnt = FetchController(fc_ui.name, fc_ui.stereo) + + flex_cnt_value = int(stack.pop(-1).value) + inputs.append((self.flex_controllers[flex_cnt_value].name, 'NWAY')) + fc_ui = flex_controllers[self.flex_controllers[flex_cnt_value].name] + multi_cnt = FetchController(fc_ui.nway_controller, fc_ui.stereo) + + # Reversed the order, revert back if it wont help + f_w = stack.pop(-1) + f_z = stack.pop(-1) + f_y = stack.pop(-1) + f_x = stack.pop(-1) + final_expr = NWay(multi_cnt, flex_cnt, f_x, f_y, f_z, f_w) + stack.append(final_expr) + elif flex_op == FlexOpType.DME_UPPER_EYELID: + close_lid_v_controller = self.flex_controllers[op.value] + inputs.append((close_lid_v_controller.name, 'DUE')) + close_lid_v = RClamp(FetchController(close_lid_v_controller.name), + close_lid_v_controller.min, close_lid_v_controller.max, + 0, 1) + + flex_cnt_value = int(stack.pop(-1).value) + close_lid_controller = self.flex_controllers[flex_cnt_value] + inputs.append((close_lid_controller.name, 'DUE')) + close_lid = RClamp(FetchController(close_lid_controller.name), + close_lid_controller.min, close_lid_controller.max, + 0, 1) + + blink_index = int(stack.pop(-1).value) + # blink = Value(0.0) + # if blink_index >= 0: + # blink_controller = self.flex_controllers[blink_index] + # inputs.append((blink_controller.name, 'DUE')) + # blink_fetch = FetchController(blink_controller.name) + # blink = CustomFunction('rclamped', blink_fetch, + # blink_controller.min, blink_controller.max, + # 0, 1) + + eye_up_down_index = int(stack.pop(-1).value) + eye_up_down = Value(0.0) + if eye_up_down_index >= 0: + eye_up_down_controller = self.flex_controllers[eye_up_down_index] + inputs.append((eye_up_down_controller.name, 'DUE')) + eye_up_down_fetch = FetchController(eye_up_down_controller.name) + eye_up_down = RClamp(eye_up_down_fetch, + eye_up_down_controller.min, eye_up_down_controller.max, + -1, 1) + + stack.append(CustomFunction('upper_eyelid_case', eye_up_down, close_lid_v, close_lid)) + elif flex_op == FlexOpType.DME_LOWER_EYELID: + close_lid_v_controller = self.flex_controllers[op.value] + inputs.append((close_lid_v_controller.name, 'DUE')) + close_lid_v = RClamp(FetchController(close_lid_v_controller.name), + close_lid_v_controller.min, close_lid_v_controller.max, + 0, 1) + + flex_cnt_value = int(stack.pop(-1).value) + close_lid_controller = self.flex_controllers[flex_cnt_value] + inputs.append((close_lid_controller.name, 'DUE')) + close_lid = RClamp(FetchController(close_lid_controller.name), + close_lid_controller.min, close_lid_controller.max, + 0, 1) + + blink_index = int(stack.pop(-1).value) + # blink = Value(0.0) + # if blink_index >= 0: + # blink_controller = self.flex_controllers[blink_index] + # inputs.append((blink_controller.name, 'DUE')) + # blink_fetch = FetchController(blink_controller.name) + # blink = CustomFunction('rclamped', blink_fetch, + # blink_controller.min, blink_controller.max, + # 0, 1) + + eye_up_down_index = int(stack.pop(-1).value) + eye_up_down = Value(0.0) + if eye_up_down_index >= 0: + eye_up_down_controller = self.flex_controllers[eye_up_down_index] + inputs.append((eye_up_down_controller.name, 'DUE')) + eye_up_down_fetch = FetchController(eye_up_down_controller.name) + eye_up_down = RClamp(eye_up_down_fetch, + eye_up_down_controller.min, eye_up_down_controller.max, + -1, 1) + + stack.append(CustomFunction('lower_eyelid_case', eye_up_down, close_lid_v, close_lid)) + elif flex_op == FlexOpType.OPEN: + continue + else: + print("Unknown OP", op) + if len(stack) > 1 or not stack: + print(f"failed to parse ({self.flex_names[rule.flex_index]}) flex rule") + print(stack) + continue + final_expr = stack.pop(-1) + name = self.flex_names[rule.flex_index] + rules[name] = (final_expr, inputs) + except Exception as ex: + traceback.print_exc() + print(f"failed to parse ({self.flex_names[rule.flex_index]}) flex rule") + print(stack) + + return rules diff --git a/library/models/mdl/v44/mdl_file.py b/library/models/mdl/v44/mdl_file.py index a3987389..853eb7b2 100644 --- a/library/models/mdl/v44/mdl_file.py +++ b/library/models/mdl/v44/mdl_file.py @@ -183,11 +183,11 @@ def rebuild_flex_rules(self): for op in rule.flex_ops: flex_op = op.op if flex_op == FlexOpType.CONST: - stack.append(Value(op.value)) + stack.append(Value(round(op.value, 4))) elif flex_op == FlexOpType.FETCH1: inputs.append((self.flex_controllers[op.value].name, 'fetch1')) fc_ui = flex_controllers[self.flex_controllers[op.value].name] - stack.append(FetchController(fc_ui.name, fc_ui.stereo)) + stack.append(FetchController(self.flex_controllers[op.value].name, fc_ui.stereo)) elif flex_op == FlexOpType.FETCH2: inputs.append((self.flex_names[op.value], 'fetch2')) stack.append(FetchFlex(self.flex_names[op.value])) @@ -212,17 +212,16 @@ def rebuild_flex_rules(self): elif flex_op == FlexOpType.TWO_WAY_0: inputs.append((self.flex_controllers[op.value].name, '2WAY0')) fc_ui = flex_controllers[self.flex_controllers[op.value].name] - stack.append(RClamp(FetchController(fc_ui.name, fc_ui.stereo), - -1, 0, 1, 0)) + stack.append(TwoWay0(FetchController(self.flex_controllers[op.value].name, fc_ui.stereo))) elif flex_op == FlexOpType.TWO_WAY_1: inputs.append((self.flex_controllers[op.value].name, '2WAY1')) fc_ui = flex_controllers[self.flex_controllers[op.value].name] - stack.append(Clamp(FetchController(fc_ui.name, fc_ui.stereo), 0, 1), ) + stack.append(TwoWay1(FetchController(self.flex_controllers[op.value].name, fc_ui.stereo))) elif flex_op == FlexOpType.NWAY: inputs.append((self.flex_controllers[op.value].name, 'NWAY')) fc_ui = flex_controllers[self.flex_controllers[op.value].name] - flex_cnt = FetchController(fc_ui.name, fc_ui.stereo) + flex_cnt = FetchController(self.flex_controllers[op.value].name, fc_ui.stereo) flex_cnt_value = int(stack.pop(-1).value) inputs.append((self.flex_controllers[flex_cnt_value].name, 'NWAY')) @@ -239,26 +238,14 @@ def rebuild_flex_rules(self): elif flex_op == FlexOpType.DME_UPPER_EYELID: close_lid_v_controller = self.flex_controllers[op.value] inputs.append((close_lid_v_controller.name, 'DUE')) - close_lid_v = RClamp(FetchController(close_lid_v_controller.name), - close_lid_v_controller.min, close_lid_v_controller.max, - 0, 1) + close_lid_v = FetchController(close_lid_v_controller.name) flex_cnt_value = int(stack.pop(-1).value) close_lid_controller = self.flex_controllers[flex_cnt_value] inputs.append((close_lid_controller.name, 'DUE')) - close_lid = RClamp(FetchController(close_lid_controller.name), - close_lid_controller.min, close_lid_controller.max, - 0, 1) + close_lid = FetchController(close_lid_controller.name) blink_index = int(stack.pop(-1).value) - # blink = Value(0.0) - # if blink_index >= 0: - # blink_controller = self.flex_controllers[blink_index] - # inputs.append((blink_controller.name, 'DUE')) - # blink_fetch = FetchController(blink_controller.name) - # blink = CustomFunction('rclamped', blink_fetch, - # blink_controller.min, blink_controller.max, - # 0, 1) eye_up_down_index = int(stack.pop(-1).value) eye_up_down = Value(0.0) @@ -266,34 +253,20 @@ def rebuild_flex_rules(self): eye_up_down_controller = self.flex_controllers[eye_up_down_index] inputs.append((eye_up_down_controller.name, 'DUE')) eye_up_down_fetch = FetchController(eye_up_down_controller.name) - eye_up_down = RClamp(eye_up_down_fetch, - eye_up_down_controller.min, eye_up_down_controller.max, - -1, 1) + eye_up_down = eye_up_down_fetch - stack.append(CustomFunction('upper_eyelid_case', eye_up_down, close_lid_v, close_lid)) + stack.append(UpperEye(eye_up_down, close_lid_v, close_lid)) elif flex_op == FlexOpType.DME_LOWER_EYELID: close_lid_v_controller = self.flex_controllers[op.value] inputs.append((close_lid_v_controller.name, 'DUE')) - close_lid_v = RClamp(FetchController(close_lid_v_controller.name), - close_lid_v_controller.min, close_lid_v_controller.max, - 0, 1) + close_lid_v = FetchController(close_lid_v_controller.name) flex_cnt_value = int(stack.pop(-1).value) close_lid_controller = self.flex_controllers[flex_cnt_value] inputs.append((close_lid_controller.name, 'DUE')) - close_lid = RClamp(FetchController(close_lid_controller.name), - close_lid_controller.min, close_lid_controller.max, - 0, 1) + close_lid = FetchController(close_lid_controller.name) blink_index = int(stack.pop(-1).value) - # blink = Value(0.0) - # if blink_index >= 0: - # blink_controller = self.flex_controllers[blink_index] - # inputs.append((blink_controller.name, 'DUE')) - # blink_fetch = FetchController(blink_controller.name) - # blink = CustomFunction('rclamped', blink_fetch, - # blink_controller.min, blink_controller.max, - # 0, 1) eye_up_down_index = int(stack.pop(-1).value) eye_up_down = Value(0.0) @@ -301,11 +274,9 @@ def rebuild_flex_rules(self): eye_up_down_controller = self.flex_controllers[eye_up_down_index] inputs.append((eye_up_down_controller.name, 'DUE')) eye_up_down_fetch = FetchController(eye_up_down_controller.name) - eye_up_down = RClamp(eye_up_down_fetch, - eye_up_down_controller.min, eye_up_down_controller.max, - -1, 1) + eye_up_down = eye_up_down_fetch - stack.append(CustomFunction('lower_eyelid_case', eye_up_down, close_lid_v, close_lid)) + stack.append(LowerEye(eye_up_down, close_lid_v, close_lid)) elif flex_op == FlexOpType.OPEN: continue else: diff --git a/library/models/mdl/v44/vertex_animation_cache.py b/library/models/mdl/v44/vertex_animation_cache.py index 66114ffc..bc55c604 100644 --- a/library/models/mdl/v44/vertex_animation_cache.py +++ b/library/models/mdl/v44/vertex_animation_cache.py @@ -12,7 +12,8 @@ # ("index", np.int32, (1,)), ("pos", np.float32, (3,)), ("normal", np.float32, (3,)), - ("wrinkle", np.float32, (1,)), + ("wrinkle", np.int16, (1,)), + ("side", np.float32, (1,)), ]) @@ -33,8 +34,12 @@ def process_mesh(mesh: Mesh, desired_lod=0): index_ = flex.vertex_animations['index'].astype(np.uint32).reshape(-1) vertex_indices = index_ + mesh.vertex_index_start + vertex_offset vertex_data = vertex_cache[flex_name] + #vertex_data["side"][:] = 1.0 vertex_data["pos"][vertex_indices] = flex.vertex_animations['vertex_delta'] vertex_data["normal"][vertex_indices] = flex.vertex_animations['normal_delta'] + vertex_data["side"][vertex_indices] = flex.vertex_animations["side"] / 255 + if flex.vertex_anim_type == 1: + vertex_data['wrinkle'][vertex_indices] = flex.vertex_animations['wrinkle_delta'] for body_part in mdl.body_parts: for model in body_part.models: diff --git a/library/models/mdl/v49/flex_expressions copy.py b/library/models/mdl/v49/flex_expressions copy.py new file mode 100644 index 00000000..9987078f --- /dev/null +++ b/library/models/mdl/v49/flex_expressions copy.py @@ -0,0 +1,251 @@ +import abc + + +class Value: + def __hash__(self): + return hash(self.value) + + def __eq__(self, o: object) -> bool: + if isinstance(o, self.__class__): + o: Value + return self.value == o.value + else: + return False + + def __init__(self, value, ): + self.value = value + + def __repr__(self): + return f'{self.value:.4}' + + def as_simple(self): + return str(self) + + +class Neg(Value): + def __repr__(self): + return f'(-{self.value})' + + def as_simple(self): + arg1 = self.value.as_simple() if isinstance(self.value, (Value, Function)) else f'({self.value})' + return f'(-{arg1})' + + +class FetchController(Value): + + def __init__(self, value, stereo=False): + self.stereo = stereo + super().__init__(value) + + def __repr__(self): + return f'{self.value.replace(" ", "_")}' + + def as_simple(self): + name = f'{self.value.replace(" ", "_")}' + if self.stereo and name.startswith('right_'): + name = name[6:] + if self.stereo and name.startswith('left_'): + name = name[5:] + return name + + +class FetchFlex(Value): + def __repr__(self): + return f'{self.value.replace(" ", "_")}' + + def as_simple(self): + return f'%{self.value.replace(" ", "_")}' + + +class Expr: + def __hash__(self) -> int: + return hash(self.left) + hash(self.right) + + def __eq__(self, o: object) -> bool: + if isinstance(o, self.__class__): + o: Expr + return self.left == o.left and self.right == o.right + else: + return False + + def __init__(self, lhs, rhs, ): + self.left = lhs + self.right = rhs + + @abc.abstractmethod + def as_simple(self): + pass + + +class Add(Expr): + def __repr__(self): + arg1 = self.left if isinstance(self.left, (Value, Function)) else f'({self.left})' + arg2 = self.right if isinstance(self.right, (Value, Function)) else f'({self.right})' + return f'{arg1} + {arg2}' + + def as_simple(self): + arg1 = self.left.as_simple() if isinstance(self.left, (Value, Function)) else f'({self.left})' + arg2 = self.right.as_simple() if isinstance(self.right, (Value, Function)) else f'({self.right})' + return f'({arg1} + {arg2})' + + +class Sub(Expr): + def __repr__(self): + arg1 = self.left if isinstance(self.left, (Value, Function)) else f'({self.left})' + arg2 = self.right if isinstance(self.right, (Value, Function)) else f'({self.right})' + return f'{arg1} - {arg2}' + + def as_simple(self): + arg1 = self.left.as_simple() if isinstance(self.left, (Value, Function)) else f'({self.left})' + arg2 = self.right.as_simple() if isinstance(self.right, (Value, Function)) else f'({self.right})' + return f'({arg1} - {arg2})' + + +class Mul(Expr): + def __repr__(self): + arg1 = self.left if isinstance(self.left, (Value, Function)) else f'({self.left})' + arg2 = self.right if isinstance(self.right, (Value, Function)) else f'({self.right})' + return f'{arg1}*{arg2}' + + def as_simple(self): + arg1 = self.left.as_simple() if isinstance(self.left, (Value, Function)) else f'({self.left})' + arg2 = self.right.as_simple() if isinstance(self.right, (Value, Function)) else f'({self.right})' + return f'({arg1} * {arg2})' + + +class Div(Expr): + def __repr__(self): + arg1 = self.left if isinstance(self.left, (Value, Function)) else f'({self.left})' + arg2 = self.right if isinstance(self.right, (Value, Function)) else f'({self.right})' + return f'{arg1}/{arg2}' + + def as_simple(self): + arg1 = self.left.as_simple() if isinstance(self.left, (Value, Function)) else f'({self.left})' + arg2 = self.right.as_simple() if isinstance(self.right, (Value, Function)) else f'({self.right})' + return f'({arg1} / {arg2})' + + +class Function: + def __hash__(self) -> int: + return hash(sum(map(hash, self.values))) + + def __eq__(self, o: object) -> bool: + if isinstance(o, self.__class__): + o: Function + return len(self.values) == len(o.values) and all(a == b for (a, b) in zip(self.values, o.values)) + else: + return False + + def __init__(self, *values): + self.values: list = list(values) + + def as_simple(self): + return "Implement me" + + +class CustomFunction(Function): + def __init__(self, function_name: str, *values): + super().__init__(*values) + self.function_name = function_name + + def __repr__(self) -> str: + return f'{self.function_name}({", ".join(map(str, self.values))})' + + +class RClamp(Function): + def as_simple(self): + arg0 = self.values[0].as_simple() if isinstance(self.values[0], (Value, Function)) else f'({self.values[0]})' + arg1 = self.values[1].as_simple() if isinstance(self.values[1], (Value, Function)) else f'({self.values[1]})' + arg2 = self.values[2].as_simple() if isinstance(self.values[2], (Value, Function)) else f'({self.values[2]})' + arg3 = self.values[3].as_simple() if isinstance(self.values[3], (Value, Function)) else f'({self.values[3]})' + arg4 = self.values[4].as_simple() if isinstance(self.values[4], (Value, Function)) else f'({self.values[4]})' + return f'RemapValClamped({arg0}, {arg1}, {arg2}, {arg3}, {arg4})' + + def __repr__(self) -> str: + return f'rclamped({self.values[0]}, {self.values[1]}, {self.values[2]}, {self.values[3]}, {self.values[4]})' + + +class Clamp(Function): + def as_simple(self): + arg0 = self.values[0].as_simple() if isinstance(self.values[0], (Value, Function)) else f'({self.values[0]})' + arg1 = self.values[1].as_simple() if isinstance(self.values[1], (Value, Function)) else f'({self.values[1]})' + arg2 = self.values[2].as_simple() if isinstance(self.values[2], (Value, Function)) else f'({self.values[2]})' + return f'min(max({arg0}, {arg1}), {arg2})' + + def __repr__(self) -> str: + return f'clamp({self.values[0]},{self.values[1]},{self.values[2]})' + + +class NWay(Function): + + def __init__(self, *values): + super().__init__(*values) + + def as_simple(self): + multi_cnt, flex_cnt, f_x, f_y, f_z, f_w = self.values + f_x = f_x.as_simple() if isinstance(f_x, (Value, Function)) else f'({f_x})' + f_y = f_y.as_simple() if isinstance(f_y, (Value, Function)) else f'({f_y})' + f_z = f_z.as_simple() if isinstance(f_z, (Value, Function)) else f'({f_z})' + f_w = f_w.as_simple() if isinstance(f_w, (Value, Function)) else f'({f_w})' + multi_cnt = multi_cnt.as_simple() if isinstance(multi_cnt, (Value, Function)) else f'({multi_cnt})' + flex_cnt = flex_cnt.as_simple() if isinstance(flex_cnt, (Value, Function)) else f'({flex_cnt})' + greater_than_x = f"min(1, (-min(0, ({f_x} - {multi_cnt} ))))" + less_than_y = f"min(1, (-min(0, ({multi_cnt} - ({f_y})))))" + remap_x = f"min(max(({flex_cnt} - {f_x}) / ({f_y} - ({f_x}) ), 0), 1)" + greater_than_equal_y = f"-(min(1, (-min(0, ({multi_cnt} - ({f_y}))))) - 1)" + less_than_equal_z = f"-(min(1, (-min(0, ({f_z} - {multi_cnt})))) - 1)" + greater_than_z = f"min(1, (-min(0, ({f_z} - {multi_cnt}))))" + less_than_w = f"min(1, (-min(0, ({multi_cnt} - ({f_w})))))" + remap_z = f"(1 - (min(max(({multi_cnt} - ({f_z})) / ({f_w} - ({f_z})), 0), 1)))" + + fl_value = f"(({greater_than_x} * {less_than_y}) * {remap_x}) + ({greater_than_equal_y} * {less_than_equal_z}) + (({greater_than_z} * {less_than_w}) * {remap_z})" + + return f"(({fl_value}) * ({flex_cnt}))" + # return f'NWay({self.values[0]},{self.values[1]},{self.values[2]},{self.values[3]},{self.values[4], {self.values[5]} })' + + def __repr__(self) -> str: + return f'nway({self.values[0]},{self.values[1]},{self.values[2]},{self.values[3]},{self.values[4], {self.values[5]} })' + + +class Max(Function): + def __repr__(self): + arg1 = self.values[0] if isinstance(self.values[0], (Value, Function)) else f'({self.values[0]})' + arg2 = self.values[1] if isinstance(self.values[1], (Value, Function)) else f'({self.values[1]})' + return f'max({arg1},{arg2})' + + def as_simple(self): + arg1 = self.values[0].as_simple() if isinstance(self.values[0], (Value, Function)) else f'({self.values[0]})' + arg2 = self.values[1].as_simple() if isinstance(self.values[1], (Value, Function)) else f'({self.values[1]})' + return f'max({arg1},{arg2})' + + +class Min(Function): + def __repr__(self): + arg1 = self.values[0] if isinstance(self.values[0], (Value, Function)) else f'({self.values[0]})' + arg2 = self.values[1] if isinstance(self.values[1], (Value, Function)) else f'({self.values[1]})' + + return f'min({arg1},{arg2})' + + def as_simple(self): + arg1 = self.values[0].as_simple() if isinstance(self.values[0], (Value, Function)) else f'({self.values[0]})' + arg2 = self.values[1].as_simple() if isinstance(self.values[1], (Value, Function)) else f'({self.values[1]})' + return f'min({arg1},{arg2})' + + +class Combo(Function): + def __repr__(self): + return f'combo({", ".join(map(str, self.values))})' + + def as_simple(self): + args = map(lambda x: x.as_simple() if isinstance(x, (Value, Function)) else f'({x})', self.values) + return f'({"*".join(args)})' + + +class Dominator(Function): + def __repr__(self): + return f'dom({self.values[0]}, {", ".join(map(str, self.values[1:]))})' + + def as_simple(self): + arg1 = self.values[-1].as_simple() if isinstance(self.values[-1], (Value, Function)) else f'({self.values[-1]})' + args = map(lambda x: x.as_simple() if isinstance(x, (Value, Function)) else f'({x})', self.values[:-1]) + return f'((1 - {arg1}) * {"*".join(args)})' diff --git a/library/models/mdl/v49/flex_expressions.py b/library/models/mdl/v49/flex_expressions.py index cb52878d..fccb28ac 100644 --- a/library/models/mdl/v49/flex_expressions.py +++ b/library/models/mdl/v49/flex_expressions.py @@ -42,10 +42,10 @@ def __repr__(self): def as_simple(self): name = f'{self.value.replace(" ", "_")}' - if self.stereo and name.startswith('right_'): - name = name[6:] - if self.stereo and name.startswith('left_'): - name = name[5:] + #if self.stereo and name.startswith('right_'): + # name = name[6:] + #if self.stereo and name.startswith('left_'): + # name = name[5:] return name @@ -81,48 +81,50 @@ class Add(Expr): def __repr__(self): arg1 = self.left if isinstance(self.left, (Value, Function)) else f'({self.left})' arg2 = self.right if isinstance(self.right, (Value, Function)) else f'({self.right})' - return f'{arg1} + {arg2}' + return f'{arg2}+{arg1}' def as_simple(self): arg1 = self.left.as_simple() if isinstance(self.left, (Value, Function)) else f'({self.left})' arg2 = self.right.as_simple() if isinstance(self.right, (Value, Function)) else f'({self.right})' - return f'({arg1} + {arg2})' + return f'({arg2}+{arg1})' class Sub(Expr): def __repr__(self): arg1 = self.left if isinstance(self.left, (Value, Function)) else f'({self.left})' arg2 = self.right if isinstance(self.right, (Value, Function)) else f'({self.right})' - return f'{arg2} - {arg1}' + return f'{arg2}-{arg1}' def as_simple(self): arg1 = self.left.as_simple() if isinstance(self.left, (Value, Function)) else f'({self.left})' arg2 = self.right.as_simple() if isinstance(self.right, (Value, Function)) else f'({self.right})' - return f'({arg1} - {arg2})' + return f'({arg2}-{arg1})' class Mul(Expr): def __repr__(self): arg1 = self.left if isinstance(self.left, (Value, Function)) else f'({self.left})' arg2 = self.right if isinstance(self.right, (Value, Function)) else f'({self.right})' - return f'{arg1}*{arg2}' + return f'{arg2}*{arg1}' def as_simple(self): arg1 = self.left.as_simple() if isinstance(self.left, (Value, Function)) else f'({self.left})' arg2 = self.right.as_simple() if isinstance(self.right, (Value, Function)) else f'({self.right})' - return f'({arg1} * {arg2})' + return f'{arg2}*{arg1}' class Div(Expr): def __repr__(self): arg1 = self.left if isinstance(self.left, (Value, Function)) else f'({self.left})' arg2 = self.right if isinstance(self.right, (Value, Function)) else f'({self.right})' - return f'{arg1}/{arg2}' + #return f'{arg2}/{arg1}' + return f'{arg2}/max(abs({arg1}),.001)*copysign(1, {arg1})' + return f'{arg2}/1 if {arg1}==0 else {arg1}' def as_simple(self): arg1 = self.left.as_simple() if isinstance(self.left, (Value, Function)) else f'({self.left})' arg2 = self.right.as_simple() if isinstance(self.right, (Value, Function)) else f'({self.right})' - return f'({arg1} / {arg2})' + return f'{arg2}/.001 if isclose({arg1}, 0) else {arg1}' class Function: @@ -167,14 +169,57 @@ def __repr__(self) -> str: class Clamp(Function): def as_simple(self): - arg0 = self.values[0].as_simple() if isinstance(self.values[0], (Value, Function)) else f'({self.values[0]})' - arg1 = self.values[1].as_simple() if isinstance(self.values[1], (Value, Function)) else f'({self.values[1]})' - arg2 = self.values[2].as_simple() if isinstance(self.values[2], (Value, Function)) else f'({self.values[2]})' + arg0 = self.values[0].as_simple() if isinstance(self.values[0], (Value, Function)) else f'{self.values[0]}' + arg1 = self.values[1].as_simple() if isinstance(self.values[1], (Value, Function)) else f'{self.values[1]}' + arg2 = self.values[2].as_simple() if isinstance(self.values[2], (Value, Function)) else f'{self.values[2]}' return f'min(max({arg0}, {arg1}), {arg2})' def __repr__(self) -> str: return f'clamp({self.values[0]},{self.values[1]},{self.values[2]})' +class TwoWay0(Function): + def as_simple(self): + arg0 = self.values[0].as_simple() if isinstance(self.values[0], (Value, Function)) else f'{self.values[0]}' + return f'clamp({arg0}*-1)' + def __repr__(self) -> str: + return f'clamp({self.values[0]}*-1)' + +class TwoWay1(Function): + def as_simple(self): + arg0 = self.values[0].as_simple() if isinstance(self.values[0], (Value, Function)) else f'{self.values[0]}' + return f'clamp({arg0})' + def __repr__(self) -> str: + return f'clamp({self.values[0]})' + +class LowerEye(Function): + def as_simple(self): + eyes, multi, flex = self.values + eyes = eyes.as_simple() if isinstance(eyes, (Value, Function)) else f'{eyes}' + multi = multi.as_simple() if isinstance(multi, (Value, Function)) else f'{multi}' + flex = flex.as_simple() if isinstance(flex, (Value, Function)) else f'{flex}' + return f'(1-abs(max({eyes}/45, 0)))*({multi}+1)/2*({flex}+1)/2' + + def __repr__(self): + eyes, multi, flex = self.values + eyes = eyes.as_simple() if isinstance(eyes, (Value, Function)) else f'{eyes}' + multi = multi.as_simple() if isinstance(multi, (Value, Function)) else f'{multi}' + flex = flex.as_simple() if isinstance(flex, (Value, Function)) else f'{flex}' + return f'LowerEye({eyes}, {multi}, {flex})' + +class UpperEye(Function): + def as_simple(self): + eyes, multi, flex = self.values + eyes = eyes.as_simple() if isinstance(eyes, (Value, Function)) else f'{eyes}' + multi = multi.as_simple() if isinstance(multi, (Value, Function)) else f'{multi}' + flex = flex.as_simple() if isinstance(flex, (Value, Function)) else f'{flex}' + return f'(1-abs(min({eyes}/45, 0)))*({multi}-1)/-2*({flex}+1)/2' + + def __repr__(self): + eyes, multi, flex = self.values + eyes = eyes.as_simple() if isinstance(eyes, (Value, Function)) else f'{eyes}' + multi = multi.as_simple() if isinstance(multi, (Value, Function)) else f'{multi}' + flex = flex.as_simple() if isinstance(flex, (Value, Function)) else f'{flex}' + return f'UpperEye({eyes}, {multi}, {flex})' class NWay(Function): @@ -183,12 +228,15 @@ def __init__(self, *values): def as_simple(self): multi_cnt, flex_cnt, f_x, f_y, f_z, f_w = self.values - f_x = f_x.as_simple() if isinstance(f_x, (Value, Function)) else f'({f_x})' - f_y = f_y.as_simple() if isinstance(f_y, (Value, Function)) else f'({f_y})' - f_z = f_z.as_simple() if isinstance(f_z, (Value, Function)) else f'({f_z})' - f_w = f_w.as_simple() if isinstance(f_w, (Value, Function)) else f'({f_w})' - multi_cnt = multi_cnt.as_simple() if isinstance(multi_cnt, (Value, Function)) else f'({multi_cnt})' - flex_cnt = flex_cnt.as_simple() if isinstance(flex_cnt, (Value, Function)) else f'({flex_cnt})' + f_x = f_x.as_simple() if isinstance(f_x, (Value, Function)) else f'{f_x}' + f_y = f_y.as_simple() if isinstance(f_y, (Value, Function)) else f'{f_y}' + f_z = f_z.as_simple() if isinstance(f_z, (Value, Function)) else f'{f_z}' + f_w = f_w.as_simple() if isinstance(f_w, (Value, Function)) else f'{f_w}' + multi_cnt = multi_cnt.as_simple() if isinstance(multi_cnt, (Value, Function)) else f'{multi_cnt}' + flex_cnt = flex_cnt.as_simple() if isinstance(flex_cnt, (Value, Function)) else f'{flex_cnt}' + + return 'max(min(({0}-{1})/({2}-{1}),({4}-{0})/({4}-{3})),0)*{5}'.format(multi_cnt, f_x, f_y, f_z, f_w, flex_cnt) + greater_than_x = f"min(1, (-min(0, ({f_x} - {multi_cnt} ))))" less_than_y = f"min(1, (-min(0, ({multi_cnt} - ({f_y})))))" remap_x = f"min(max(({flex_cnt} - {f_x}) / ({f_y} - ({f_x}) ), 0), 1)" @@ -248,4 +296,5 @@ def __repr__(self): def as_simple(self): arg1 = self.values[-1].as_simple() if isinstance(self.values[-1], (Value, Function)) else f'({self.values[-1]})' args = map(lambda x: x.as_simple() if isinstance(x, (Value, Function)) else f'({x})', self.values[:-1]) - return f'((1 - {arg1}) * {"*".join(args)})' + #return f'((1-{arg1})*{"*".join(args)})' + return f'{arg1}*' + '*'.join([f'(1-{var})' for var in args])