From c1e0f4a1bf5219ef9ed8ade22014913121d58114 Mon Sep 17 00:00:00 2001 From: Lila Date: Thu, 10 Oct 2024 12:00:37 +0100 Subject: [PATCH] WIP --- .../f3d/bsdf_converter/converter.py | 305 +++++++++++++++++- .../f3d/bsdf_converter/operators.py | 91 ++++-- .../f3d/bsdf_converter/properties.py | 5 +- fast64_internal/f3d/bsdf_converter/ui.py | 18 +- fast64_internal/f3d/f3d_material.py | 30 +- 5 files changed, 399 insertions(+), 50 deletions(-) diff --git a/fast64_internal/f3d/bsdf_converter/converter.py b/fast64_internal/f3d/bsdf_converter/converter.py index d1d2bd9ed..75e8bd41e 100644 --- a/fast64_internal/f3d/bsdf_converter/converter.py +++ b/fast64_internal/f3d/bsdf_converter/converter.py @@ -1,20 +1,309 @@ -from bpy.types import Object +import bpy +from bpy.types import Object, Material import dataclasses +from ...utility import get_clean_color, PluginError +from ..f3d_material import ( + combiner_uses, + get_output_method, + is_mat_f3d, + all_combiner_uses, + set_blend_to_output_method, + trunc_10_2, + F3DMaterialProperty, + RDPSettings, + TextureProperty, + CombinerProperty, +) + + +# Ideally we'd use mathutils.Color here but it does not support alpha (and mul for some reason) +@dataclasses.dataclass +class Color: + r: float = 0.0 + g: float = 0.0 + b: float = 0.0 + a: float = 0.0 + + def wrap(self, min_value: float, max_value: float): + def wrap_value(value, min_value=min_value, max_value=max_value): + range_width = max_value - min_value + return ((value - min_value) % range_width) + min_value + + return Color(wrap_value(self.r), wrap_value(self.g), wrap_value(self.b), wrap_value(self.a)) + + def to_clean_list(self): + def round_and_clamp(value): + return round(max(min(value, 1.0), 0.0), 4) + + return [ + round_and_clamp(self.r), + round_and_clamp(self.g), + round_and_clamp(self.b), + round_and_clamp(self.a), + ] + + def __sub__(self, other): + return Color(self.r - other.r, self.g - other.g, self.b - other.b, self.a - other.a) + + def __add__(self, other): + return Color(self.r + other.r, self.g + other.g, self.b + other.b, self.a + other.a) + + def __mul__(self, other): + return Color(self.r * other.r, self.g * other.g, self.b * other.b, self.a * other.a) + + +def get_color_component(inp: str, f3d_mat: F3DMaterialProperty, previous_alpha: float) -> float: + if inp == "0": + return 0.0 + elif inp == "1": + return 1.0 + elif inp.startswith("COMBINED"): + return previous_alpha + elif inp == "LOD_FRACTION": + return 0.0 # Fast64 always uses black, let's do that for now + elif inp == "PRIM_LOD_FRAC": + return f3d_mat.prim_lod_frac + elif inp == "PRIMITIVE_ALPHA": + return f3d_mat.prim_color[3] + elif inp == "ENV_ALPHA": + return f3d_mat.env_color[3] + elif inp == "K4": + return f3d_mat.k4 + elif inp == "K5": + return f3d_mat.k5 + + +def get_color_from_input( + inp: str, previous_color: Color, f3d_mat: F3DMaterialProperty, is_alpha: bool, default_color: Color +) -> Color: + if inp == "COMBINED" and not is_alpha: + return previous_color + elif inp == "CENTER": + return Color(*get_clean_color(f3d_mat.key_center), previous_color.a) + elif inp == "SCALE": + return Color(*list(f3d_mat.key_scale), previous_color.a) + elif inp == "PRIMITIVE": + return Color(*get_clean_color(f3d_mat.prim_color, True)) + elif inp == "ENVIRONMENT": + return Color(*get_clean_color(f3d_mat.env_color, True)) + elif inp == "SHADE": + if f3d_mat.rdp_settings.g_lighting and f3d_mat.set_lights and f3d_mat.use_default_lighting: + return Color(*get_clean_color(f3d_mat.default_light_color), previous_color.a) + return Color(1.0, 1.0, 1.0, previous_color.a) + else: + value = get_color_component(inp, f3d_mat, previous_color.a) + if value is not None: + return Color(value, value, value, value) + return default_color + + +def fake_color_from_cycle(cycle: list[str], previous_color: Color, f3d_mat: F3DMaterialProperty, is_alpha=False): + default_colors = [Color(1.0, 1.0, 1.0, 1.0), Color(), Color(1.0, 1.0, 1.0, 1.0), Color()] + a, b, c, d = [ + get_color_from_input(inp, previous_color, f3d_mat, is_alpha, default_color) + for inp, default_color in zip(cycle, default_colors) + ] + sign_extended_c = c.wrap(-1.0, 1.0001) + unwrapped_result = (a - b) * sign_extended_c + d + result = unwrapped_result.wrap(-0.5, 1.5) + if is_alpha: + result = Color(previous_color.r, previous_color.g, previous_color.b, result.a) + return result + + +def get_fake_color(f3d_mat: F3DMaterialProperty): + """Try to emulate solid colors""" + fake_color = Color() + cycle: CombinerProperty + combiners = [f3d_mat.combiner1] + if f3d_mat.rdp_settings.g_mdsft_cycletype == "G_CYC_2CYCLE": + combiners.append(f3d_mat.combiner2) + for cycle in combiners: + fake_color = fake_color_from_cycle([cycle.A, cycle.B, cycle.C, cycle.D], fake_color, f3d_mat) + fake_color = fake_color_from_cycle( + [cycle.A_alpha, cycle.B_alpha, cycle.C_alpha, cycle.D_alpha], fake_color, f3d_mat, True + ) + return fake_color.to_clean_list() + @dataclasses.dataclass -class SimpleN64Texture: - name: str +class AbstractedN64Texture: + """Very abstracted representation of a N64 texture""" + + tex: bpy.types.Image + offset: tuple[float, float] = (0.0, 0.0) + scale: tuple[float, float] = (1.0, 1.0) + repeat: bool = False @dataclasses.dataclass -class SimpleN64Material: - name: str +class AbstractedN64Material: + """Very abstracted representation of a N64 material""" + + lighting: bool = False + uv_geo: bool = False + point_filtering: bool = False + output_method: str = "OPA" + backface_culling: bool = False + color: Color = dataclasses.field(default_factory=Color) + textures: list[AbstractedN64Texture] = dataclasses.field(default_factory=list) + texture_sets_col: bool = False + texture_sets_alpha: bool = False + + @property + def main_texture(self): + return self.textures[0] if self.textures else None + + +def f3d_tex_to_abstracted(f3d_tex: TextureProperty): + def to_offset(low: float, tex_size: int): + offset = -trunc_10_2(low) * (1.0 / tex_size) + if offset == -0.0: + offset = 0.0 + return offset + if f3d_tex.tex is None: + raise PluginError("No texture set") -def obj_to_f3d(obj: Object): + abstracted_tex = AbstractedN64Texture(f3d_tex.tex, repeat=not f3d_tex.S.clamp or not f3d_tex.T.clamp) + size = f3d_tex.get_tex_size() + if size != [0, 0]: + abstracted_tex.offset = (to_offset(f3d_tex.S.low, size[0]), to_offset(f3d_tex.T.low, size[1])) + abstracted_tex.scale = (2.0 ** (f3d_tex.S.shift * -1.0), 2.0 ** (f3d_tex.T.shift * -1.0)) + + return abstracted_tex + + +def f3d_mat_to_abstracted(material: Material): + f3d_mat: F3DMaterialProperty = material.f3d_mat + rdp: RDPSettings = f3d_mat.rdp_settings + use_dict = all_combiner_uses(f3d_mat) + textures = [f3d_mat.tex0] if use_dict["Texture 0"] and f3d_mat.tex0.tex_set else [] + textures += [f3d_mat.tex1] if use_dict["Texture 1"] and f3d_mat.tex1.tex_set else [] + + abstracted_mat = AbstractedN64Material( + rdp.g_lighting, + rdp.g_tex_gen, + rdp.g_mdsft_text_filt == "G_TF_POINT", + get_output_method(material, True), + rdp.g_cull_back, + get_fake_color(f3d_mat), + ) + for i in range(2): + tex_prop = getattr(f3d_mat, f"tex{i}") + check_list = [f"TEXEL{i}", f"TEXEL{i}_ALPHA"] + sets_color = combiner_uses(f3d_mat, check_list, checkColor=True, checkAlpha=False) + sets_alpha = combiner_uses(f3d_mat, check_list, checkColor=False, checkAlpha=True) + if sets_color or sets_alpha: + abstracted_mat.textures.append(f3d_tex_to_abstracted(tex_prop)) + abstracted_mat.texture_sets_col |= sets_color + abstracted_mat.texture_sets_alpha |= sets_alpha + return abstracted_mat + + +def material_to_bsdf(material: Material): + abstracted_mat = f3d_mat_to_abstracted(material) + + new_material = bpy.data.materials.new(name=material.name) + new_material.use_nodes = True + node_tree = new_material.node_tree + nodes = node_tree.nodes + links = node_tree.links + nodes.clear() + + set_blend_to_output_method(new_material, abstracted_mat.output_method) + new_material.use_backface_culling = abstracted_mat.backface_culling + + node_x = node_y = 0 + + output_node = nodes.new(type="ShaderNodeOutputMaterial") + node_x -= 300 + node_y -= 25 + + # final shader node + if abstracted_mat.lighting: + shader_node = nodes.new(type="ShaderNodeBsdfPrincipled") + else: + shader_node = nodes.new(type="ShaderNodeEmission") + shader_node.location = (node_x, node_y) + node_x -= 300 + links.new(shader_node.outputs[0], output_node.inputs[0]) + + # texture nodes + tex_node_y = node_y + if abstracted_mat.textures: + if abstracted_mat.uv_geo: + uvmap_node = nodes.new(type="ShaderNodeTexCoord") + uvmap_output = uvmap_node.outputs["Camera"] + else: + uvmap_node = nodes.new(type="ShaderNodeUVMap") + uvmap_node.uv_map = "UVMap" + uvmap_output = uvmap_node.outputs["UV"] + uvmap_node.location = (node_x - 200, tex_node_y) + + texture_nodes = [] + for abstracted_tex in abstracted_mat.textures: + tex_node = nodes.new(type="ShaderNodeTexImage") + tex_node.location = (node_x, tex_node_y) + tex_node.image = abstracted_tex.tex + tex_node.extension = "REPEAT" if abstracted_tex.repeat else "EXTEND" + tex_node.interpolation = "Closest" if abstracted_mat.point_filtering else "Linear" + texture_nodes.append(tex_node) + + if abstracted_tex.offset != (0.0, 0.0) or abstracted_tex.scale != (1.0, 1.0): + uvmap_node.location = (node_x - 400, uvmap_node.location[1]) + + mapping_node = nodes.new(type="ShaderNodeMapping") + mapping_node.vector_type = "POINT" + mapping_node.location = (node_x - 200, tex_node_y) + mapping_node.inputs["Location"].default_value = abstracted_tex.offset + (0.0,) + mapping_node.inputs["Scale"].default_value = abstracted_tex.scale + (1.0,) + links.new(uvmap_output, mapping_node.inputs[0]) + links.new(mapping_node.outputs[0], tex_node.inputs[0]) + else: + links.new(uvmap_output, tex_node.inputs[0]) + + tex_node_y -= 300 + + if abstracted_mat.texture_sets_col: + links.new(texture_nodes[0].outputs[0], shader_node.inputs["Base Color"]) + else: + shader_node.inputs["Base Color"].default_value = abstracted_mat.color[:3] + [1.0] + if abstracted_mat.texture_sets_alpha: + links.new(texture_nodes[0].outputs[1], shader_node.inputs["Alpha"]) + else: + shader_node.inputs["Alpha"].default_value = abstracted_mat.color[3] + + return new_material + + +def material_to_f3d(material: Material): + pass + + +def obj_to_f3d(obj: Object, materials: dict[Material, Material]): + assert obj.type == "MESH" print(f"Converting BSDF materials in {obj.name}") + for index, material_slot in enumerate(obj.material_slots): + material = material_slot.material + if material is None or is_mat_f3d(material): + continue + if material in materials: + obj.material_slots[index].material = materials[material] + else: + obj.material_slots[index].material = material_to_f3d(material) + -def obj_to_bsdf(obj: Object): - print(f"Converting F3D materials in {obj.name}") \ No newline at end of file +def obj_to_bsdf(obj: Object, materials: dict[Material, Material]): + assert obj.type == "MESH" + print(f"Converting F3D materials in {obj.name}") + for index, material_slot in enumerate(obj.material_slots): + material = material_slot.material + if material is None or not is_mat_f3d(material): + continue + if material in materials: + obj.material_slots[index].material = materials[material] + else: + obj.material_slots[index].material = material_to_bsdf(material) diff --git a/fast64_internal/f3d/bsdf_converter/operators.py b/fast64_internal/f3d/bsdf_converter/operators.py index 95722e597..a02f1270f 100644 --- a/fast64_internal/f3d/bsdf_converter/operators.py +++ b/fast64_internal/f3d/bsdf_converter/operators.py @@ -1,51 +1,92 @@ +import copy + +import bpy from bpy.utils import register_class, unregister_class -from bpy.props import EnumProperty -from bpy.types import Context +from bpy.props import EnumProperty, BoolProperty +from bpy.types import Context, Object, Material from ...operators import OperatorBase +from ...utility import PluginError from .converter import obj_to_f3d, obj_to_bsdf converter_enum = [("Object", "Selected Objects", "Object"), ("Scene", "Scene", "Scene")] -class F3D_ConvertF3DToBSDF(OperatorBase): +class F3D_ConvertBSDF(OperatorBase): bl_idname = "scene.f3d_convert_to_bsdf" bl_label = "Convert F3D to BSDF" bl_options = {"REGISTER", "UNDO", "PRESET"} icon = "MATERIAL" + direction: EnumProperty(items=[("F3D", "BSDF To F3D", "F3D"), ("BSDF", "F3D To BSDF", "BSDF")]) converter_type: EnumProperty(items=converter_enum) + backup: BoolProperty(default=True, name="Backup") def execute_operator(self, context: Context): - if self.converter_type == "Object": - for obj in context.selected_objects: - obj_to_f3d(obj) - elif self.converter_type == "Scene": - for obj in context.scene.objects: - obj_to_f3d(obj) - self.report({"INFO"}, "Done.") - + collection = context.scene.collection + view_layer = context.view_layer + scene = context.scene -class F3D_ConvertBSDFToF3D(OperatorBase): - bl_idname = "scene.bsdf_convert_to_f3d" - bl_label = "Convert BSDF to F3D" - bl_options = {"REGISTER", "UNDO", "PRESET"} - icon = "NODE_MATERIAL" - - converter_type: EnumProperty(items=converter_enum) - - def execute_operator(self, context: Context): if self.converter_type == "Object": - for obj in context.selected_objects: - obj_to_bsdf(obj) + objs = context.selected_objects elif self.converter_type == "Scene": - for obj in context.scene.objects: - obj_to_bsdf(obj) + objs = scene.objects + + if not objs: + raise PluginError("No objects to convert.") + + objs: list[Object] = [obj for obj in objs if obj.type == "MESH"] + original_names = [obj.name for obj in objs] + new_objs: list[Object] = [] + backup_collection = None + + try: + materials: dict[Material, Material] = {} + for old_obj in objs: + obj = old_obj.copy() + obj.data = old_obj.data.copy() + new_objs.append(obj) + if self.direction == "F3D": + obj_to_f3d(obj, materials) + elif self.direction == "BSDF": + obj_to_bsdf(obj, materials) + + bpy.ops.object.select_all(action="DESELECT") + if self.backup: + backup_collection = bpy.data.collections.new("BSDF <-> F3D Backup") + scene.collection.children.link(backup_collection) + + for old_obj, obj, name in zip(objs, new_objs, original_names): + for collection in copy.copy(old_obj.users_collection): + collection.objects.link(obj) + collection.objects.unlink(old_obj) # remove old object from current collection + view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.make_single_user(type="SELECTED_OBJECTS") + obj.select_set(False) + + obj.name = name + if self.backup: + old_obj.name = f"{name}_backup" + backup_collection.objects.link(old_obj) + view_layer.objects.active = old_obj + else: + bpy.data.objects.remove(old_obj) + if self.backup: + for layer_collection in view_layer.layer_collection.children: + if layer_collection.collection == backup_collection: + layer_collection.exclude = True + except Exception as exc: + for obj in new_objs: + bpy.data.objects.remove(obj) + if backup_collection is not None: + bpy.data.collections.remove(backup_collection) + raise exc self.report({"INFO"}, "Done.") -classes = (F3D_ConvertF3DToBSDF, F3D_ConvertBSDFToF3D) +classes = (F3D_ConvertBSDF,) def bsdf_converter_ops_register(): diff --git a/fast64_internal/f3d/bsdf_converter/properties.py b/fast64_internal/f3d/bsdf_converter/properties.py index 52aef05fd..8db3e76df 100644 --- a/fast64_internal/f3d/bsdf_converter/properties.py +++ b/fast64_internal/f3d/bsdf_converter/properties.py @@ -1,6 +1,6 @@ from bpy.utils import register_class, unregister_class from bpy.types import PropertyGroup, UILayout -from bpy.props import EnumProperty +from bpy.props import EnumProperty, BoolProperty from ...utility import prop_split, multilineLabel @@ -11,11 +11,12 @@ class F3D_BSDFConverterProperties(PropertyGroup): """ converter_type: EnumProperty(items=[("Object", "Selected Objects", "Object"), ("Scene", "Scene", "Scene")]) + backup: BoolProperty(default=True, name="Backup") def draw_props(self, layout: UILayout): col = layout.column() prop_split(col, self, "converter_type", "Converter Type") - + col.prop(self, "backup") classes = (F3D_BSDFConverterProperties,) diff --git a/fast64_internal/f3d/bsdf_converter/ui.py b/fast64_internal/f3d/bsdf_converter/ui.py index 0719929fd..45b8b8bfb 100644 --- a/fast64_internal/f3d/bsdf_converter/ui.py +++ b/fast64_internal/f3d/bsdf_converter/ui.py @@ -2,11 +2,21 @@ from bpy.types import UILayout, Context -from .operators import F3D_ConvertF3DToBSDF, F3D_ConvertBSDFToF3D +from .operators import F3D_ConvertBSDF +from .properties import F3D_BSDFConverterProperties def bsdf_converter_panel_draw(layout: UILayout, context: Context): col = layout.column() - context.scene.fast64.f3d.bsdf_converter.draw_props(col) - F3D_ConvertF3DToBSDF.draw_props(col) - F3D_ConvertBSDFToF3D.draw_props(col) + bsdf_converter: F3D_BSDFConverterProperties = context.scene.fast64.f3d.bsdf_converter + bsdf_converter.draw_props(col) + + for direction in ("F3D", "BSDF"): + opposite = "BSDF" if direction == "F3D" else "F3D" + F3D_ConvertBSDF.draw_props( + col, + text=f"Convert {opposite} to {direction}", + direction=direction, + converter_type=bsdf_converter.converter_type, + backup=bsdf_converter.backup, + ) diff --git a/fast64_internal/f3d/f3d_material.py b/fast64_internal/f3d/f3d_material.py index 1f0970553..99ad2d157 100644 --- a/fast64_internal/f3d/f3d_material.py +++ b/fast64_internal/f3d/f3d_material.py @@ -145,6 +145,11 @@ } +def is_mat_f3d(material: Material): + assert material is None or isinstance(material, Material) + return material.is_f3d and material.mat_ver >= F3D_MAT_CUR_VERSION + + def getDefaultMaterialPreset(category): game = bpy.context.scene.gameEditorMode if game in defaultMaterialPresets[category]: @@ -303,9 +308,11 @@ def is_blender_doing_fog(settings: "RDPSettings") -> bool: ) -def get_output_method(material: bpy.types.Material) -> str: +def get_output_method(material: bpy.types.Material, check_decal=False) -> str: rendermode_preset_to_advanced(material) # Make sure advanced settings are updated settings = material.f3d_mat.rdp_settings + if check_decal and settings.zmode == "ZMODE_DEC": + return "DECAL" if settings.cvg_x_alpha: return "CLIP" if settings.force_bl and is_blender_equation_equal( @@ -315,23 +322,24 @@ def get_output_method(material: bpy.types.Material) -> str: return "OPA" -def update_blend_method(material: Material, context): - blend_mode = get_output_method(material) - if material.f3d_mat.rdp_settings.zmode == "ZMODE_DEC": - blend_mode = "DECAL" +def set_blend_to_output_method(material: Material, output_method: str): if bpy.app.version >= (4, 2, 0): - if blend_mode == "CLIP": + if output_method == "CLIP": material.surface_render_method = "DITHERED" else: material.surface_render_method = "BLENDED" - elif blend_mode == "OPA": + elif output_method == "OPA": material.blend_method = "OPAQUE" - elif blend_mode == "CLIP": + elif output_method == "CLIP": material.blend_method = "CLIP" - elif blend_mode in {"XLU", "DECAL"}: + elif output_method in {"XLU", "DECAL"}: material.blend_method = "BLEND" +def update_blend_method(material: Material, _context): + set_blend_to_output_method(material, get_output_method(material, True)) + + class DrawLayerProperty(PropertyGroup): sm64: bpy.props.EnumProperty(items=sm64EnumDrawLayers, default="1", update=update_draw_layer) oot: bpy.props.EnumProperty(items=ootEnumDrawLayers, default="Opaque", update=update_draw_layer) @@ -2865,9 +2873,9 @@ class TextureProperty(PropertyGroup): def get_tex_size(self) -> list[int]: if self.tex or self.use_tex_reference: if self.tex is not None: - return self.tex.size + return list(self.tex.size) else: - return self.tex_reference_size + return list(self.tex_reference_size) return [0, 0] def key(self):