diff --git a/__init__.py b/__init__.py index d3e5c548c..a4fecfe88 100644 --- a/__init__.py +++ b/__init__.py @@ -26,6 +26,7 @@ from .fast64_internal.mk64 import MK64_Properties, mk64_register, mk64_unregister +from .fast64_internal.f3d import F3D_Properties, f3d_register, f3d_unregister from .fast64_internal.f3d.f3d_material import ( F3D_MAT_CUR_VERSION, mat_register, @@ -37,15 +38,13 @@ from .fast64_internal.f3d.f3d_parser import f3d_parser_register, f3d_parser_unregister from .fast64_internal.f3d.flipbook import flipbook_register, flipbook_unregister from .fast64_internal.f3d.op_largetexture import op_largetexture_register, op_largetexture_unregister, ui_oplargetexture - from .fast64_internal.f3d_material_converter import ( - MatUpdateConvert, + mat_updater_draw, upgrade_f3d_version_all_meshes, - bsdf_conv_register, - bsdf_conv_unregister, - bsdf_conv_panel_regsiter, - bsdf_conv_panel_unregsiter, + mat_updater_register, + mat_updater_unregister, ) +from .fast64_internal.f3d.bsdf_converter import bsdf_converter_panel_draw from .fast64_internal.render_settings import ( Fast64RenderSettings_Properties, @@ -152,6 +151,18 @@ def draw(self, context): col.operator(ArmatureApplyWithMeshOperator.bl_idname) # col.operator(CreateMetarig.bl_idname) ui_oplargetexture(col, context) + col.separator() + + box = col.box().column() + box.label(text="Material Updater") + mat_updater_draw(box, context) + col.separator() + + box = col.box().column() + box.label(text="BSDF Converter") + bsdf_converter_panel_draw(box, context) + col.separator() + addon_updater_ops.update_notice_box_ui(self, context) @@ -223,6 +234,7 @@ class Fast64_Properties(bpy.types.PropertyGroup): sm64: bpy.props.PointerProperty(type=SM64_Properties, name="SM64 Properties") oot: bpy.props.PointerProperty(type=OOT_Properties, name="OOT Properties") mk64: bpy.props.PointerProperty(type=MK64_Properties, name="MK64 Properties") + f3d: bpy.props.PointerProperty(type=F3D_Properties, name="F3D Properties") settings: bpy.props.PointerProperty(type=Fast64Settings_Properties, name="Fast64 Settings") renderSettings: bpy.props.PointerProperty(type=Fast64RenderSettings_Properties, name="Fast64 Render Settings") @@ -420,7 +432,8 @@ def register(): utility_anim_register() mat_register() render_engine_register() - bsdf_conv_register() + mat_updater_register() + f3d_register(True) sm64_register(True) oot_register(True) mk64_register(True) @@ -430,7 +443,6 @@ def register(): for cls in classes: register_class(cls) - bsdf_conv_panel_regsiter() f3d_writer_register() flipbook_register() f3d_parser_register() @@ -471,9 +483,9 @@ def unregister(): sm64_unregister(True) oot_unregister(True) mk64_unregister(True) + f3d_unregister(True) mat_unregister() - bsdf_conv_unregister() - bsdf_conv_panel_unregsiter() + mat_updater_unregister() render_engine_unregister() del bpy.types.Scene.fullTraceback diff --git a/fast64_internal/__init__.py b/fast64_internal/__init__.py index 6bb605c2a..ade64e5ee 100644 --- a/fast64_internal/__init__.py +++ b/fast64_internal/__init__.py @@ -1,5 +1,4 @@ from .f3d_material_converter import * -from .f3d import * from .sm64 import * from .oot import * # is this really needed? from .panels import * diff --git a/fast64_internal/f3d/__init__.py b/fast64_internal/f3d/__init__.py index 9975de5d3..920e6adbf 100644 --- a/fast64_internal/f3d/__init__.py +++ b/fast64_internal/f3d/__init__.py @@ -1,4 +1,32 @@ +from bpy.types import PropertyGroup +from bpy.props import PointerProperty + from .f3d_parser import * from .f3d_material import * from .f3d_render_engine import * from .f3d_gbi import * +from .bsdf_converter import F3D_BSDFConverterProperties, bsdf_converter_register, bsdf_converter_unregister + + +class F3D_Properties(PropertyGroup): + """ + Properties in scene.fast64.f3d. + All new scene f3d properties should be children of this property group. + """ + + bsdf_converter: PointerProperty(name="BSDF Converter", type=F3D_BSDFConverterProperties) + + +classes = (F3D_Properties,) + + +def f3d_register(register_panel=True): + bsdf_converter_register() + for cls in classes: + register_class(cls) + + +def f3d_unregister(register_panel=True): + for cls in reversed(classes): + unregister_class(cls) + bsdf_converter_unregister() diff --git a/fast64_internal/f3d/bsdf_converter/__init__.py b/fast64_internal/f3d/bsdf_converter/__init__.py new file mode 100644 index 000000000..b3540e514 --- /dev/null +++ b/fast64_internal/f3d/bsdf_converter/__init__.py @@ -0,0 +1,13 @@ +from .operators import bsdf_converter_ops_register, bsdf_converter_ops_unregister +from .ui import bsdf_converter_panel_draw +from .properties import F3D_BSDFConverterProperties, bsdf_converter_props_register, bsdf_converter_props_unregister + + +def bsdf_converter_register(): + bsdf_converter_ops_register() + bsdf_converter_props_register() + + +def bsdf_converter_unregister(): + bsdf_converter_ops_unregister() + bsdf_converter_props_unregister() diff --git a/fast64_internal/f3d/bsdf_converter/converter.py b/fast64_internal/f3d/bsdf_converter/converter.py new file mode 100644 index 000000000..2dd59f5fa --- /dev/null +++ b/fast64_internal/f3d/bsdf_converter/converter.py @@ -0,0 +1,986 @@ +import copy +import math +import typing +import dataclasses +import numpy as np + +import bpy +from bpy.types import ( + Mesh, + Object, + Material, + ShaderNodeOutputMaterial, + ShaderNodeBsdfPrincipled, + ShaderNodeMixShader, + ShaderNodeBsdfTransparent, + ShaderNodeBackground, + ShaderNodeMath, + ShaderNodeMixRGB, + ShaderNodeVertexColor, + ShaderNodeTexCoord, + ShaderNodeUVMap, + ShaderNodeTexImage, + ShaderNodeMapping, + ShaderNode, +) + +from ...utility import get_clean_color, colorToLuminance, PluginError +from ..f3d_material import ( + combiner_uses, + createF3DMat, + get_output_method, + getDefaultMaterialPreset, + is_mat_f3d, + all_combiner_uses, + set_blend_to_output_method, + trunc_10_2, + update_all_node_values, + convertColorAttribute, + F3DMaterialProperty, + RDPSettings, + TextureProperty, + TextureFieldProperty, + CombinerProperty, +) +from ..f3d_gbi import isUcodeF3DEX3 +from ..f3d_writer import getColorLayer + +# Ideally we'd use mathutils.Color here but it does not support alpha (and mul for some reason) +@dataclasses.dataclass +class Color: + r: float = 1.0 + g: float = 1.0 + b: float = 1.0 + a: float = 1.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 __iter__(self): + yield self.r + yield self.g + yield self.b + yield self.a + + def __getitem__(self, key: int): + return list(self)[key] + + +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) -> 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 Color(1.0, 1.0, 1.0, 1.0) + + +def fake_color_from_cycle(cycle: list[str], previous_color: Color, f3d_mat: F3DMaterialProperty, is_alpha=False): + a, b, c, d = [get_color_from_input(inp, previous_color, f3d_mat, is_alpha) for inp in cycle] + 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 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 + set_color: bool = False + set_alpha: bool = False + packed_alpha: bool = False + alpha_as_color: bool = False # tex0 alpha, gathered in bsdf to f3d + + +@dataclasses.dataclass +class AbstractedN64Material: + """Very abstracted representation of a N64 material""" + + lighting: bool = False + uv_gen: bool = False + point_filtering: bool = False + vertex_color: str | None | bool = False + vertex_alpha: str | None | bool = False + alpha_is_median: bool = False + backface_culling: bool = False + output_method: str = "OPA" + 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 + uv_map: str = "" + + @property + def main_texture(self): + return self.textures[0] if self.textures else None + + +def f3d_tex_to_abstracted(f3d_tex: TextureProperty, set_color: bool, set_alpha: bool): + 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") + + 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)) + abstracted_tex.set_color, abstracted_tex.set_alpha = set_color, set_alpha + abstracted_tex.packed_alpha = f3d_tex.tex_format in {"I4", "I8"} + + 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 [] + g_packed_normals = rdp.g_packed_normals if isUcodeF3DEX3(bpy.context.scene.f3d_type) else False + abstracted_mat = AbstractedN64Material( + rdp.g_lighting and use_dict["Shade"], + rdp.g_tex_gen and rdp.g_lighting, + rdp.g_mdsft_text_filt == "G_TF_POINT", + (not rdp.g_lighting or g_packed_normals) and combiner_uses(f3d_mat, ["SHADE"], checkAlpha=False), + not rdp.g_fog and combiner_uses(f3d_mat, ["SHADE"], checkColor=False), + False, + rdp.g_cull_back, + get_output_method(material, True), + 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, sets_color, sets_alpha)) + abstracted_mat.texture_sets_col |= sets_color + abstracted_mat.texture_sets_alpha |= sets_alpha + # print(abstracted_mat) + return abstracted_mat + + +def material_to_bsdf(material: Material, put_alpha_into_color=False): + abstracted_mat = f3d_mat_to_abstracted(material) + + new_material = bpy.data.materials.new(name=material.name) + new_material.use_nodes = True + nodes = new_material.node_tree.nodes + links = new_material.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 + new_material.alpha_threshold = 0.125 + + node_x = node_y = alpha_y_offset = 0 + + def set_location(node, set_x=False, x_offset=0, y_offset=0): # some polish stuff + nonlocal node_x, node_y + node.location = (node_x - node.width + x_offset, node_y + y_offset) + if set_x: + node_x -= padded_from_node(node) + + def padded_from_node(node): + return node.width + 50 + + T = typing.TypeVar("T") + + def create_node(typ: T, name: str, location=False, x_offset=0, y_offset=0): + node: T = nodes.new(typ.__name__) + node.name = node.label = name + set_location(node, location, x_offset=x_offset, y_offset=y_offset) + return node + + output_node = create_node(ShaderNodeOutputMaterial, "Output", True) + node_y -= 25 + + # final shader node + if abstracted_mat.lighting: + print("Creating bsdf principled shader node") + shader_node = create_node(ShaderNodeBsdfPrincipled, "Shader", True) + links.new(shader_node.outputs[0], output_node.inputs[0]) + alpha_input = shader_node.inputs["Alpha"] + color_input = shader_node.inputs["Base Color"] + if bpy.data.version >= (4, 2, 0): + node_y -= 22 + alpha_y_offset -= 88 + else: + node_y -= 80 + alpha_y_offset -= 462 + else: # use a mix shader of transparent bsdf and background and use fac as alpha + print("Creating unlit shader node") + mix_shader = create_node(ShaderNodeMixShader, "Mix Shader", True) + links.new(mix_shader.outputs[0], output_node.inputs[0]) + alpha_input = mix_shader.inputs["Fac"] + + transparent_node = create_node(ShaderNodeBsdfTransparent, "Transparency Shader", y_offset=-47) + links.new(transparent_node.outputs[0], mix_shader.inputs[1]) + alpha_y_offset += transparent_node.height + 47 + + background_node = create_node( + ShaderNodeBackground, "Background Shader", True, y_offset=-47 - transparent_node.height + ) + links.new(background_node.outputs[0], mix_shader.inputs[2]) + color_input = background_node.inputs["Color"] + node_y -= 172 + + # cutout is removed in 4.2, it relies on the math node, glTF exporter supports this of course. + if bpy.app.version >= (4, 2, 0) and abstracted_mat.output_method == "CLIP": + print("Creating alpha clip node") + alpha_clip = create_node(ShaderNodeMath, "Alpha Clip", True, y_offset=alpha_y_offset) + alpha_clip.operation = "GREATER_THAN" + alpha_clip.use_clamp = True + alpha_clip.inputs[1].default_value = 0.125 + links.new(alpha_clip.outputs[0], alpha_input) + alpha_input = alpha_clip.inputs[0] + + vertex_color = None + vertex_color_mul = None + if abstracted_mat.vertex_color: # create vertex color mul node + print("Creating vertex color node, mix rgb node and setting color input") + vertex_color_mul = create_node(ShaderNodeMixRGB, "Vertex Color Mul", True) + vertex_color_mul.use_clamp, vertex_color_mul.blend_type = True, "MULTIPLY" + vertex_color_mul.inputs[0].default_value = 1 + links.new(vertex_color_mul.outputs[0], color_input) + color_input = vertex_color_mul.inputs[2] + if abstracted_mat.vertex_alpha: # create vertex alpha mul node + print("Creating vertex alpha node, mul node and setting color input") + vertex_alpha_mul = create_node(ShaderNodeMath, "Vertex Alpha Mul", True, y_offset=alpha_y_offset) + vertex_alpha_mul.use_clamp, vertex_alpha_mul.operation = True, "MULTIPLY" + links.new(vertex_alpha_mul.outputs[0], alpha_input) + alpha_input = vertex_alpha_mul.inputs[1] + + # create vertex color node + if abstracted_mat.vertex_color or (put_alpha_into_color and abstracted_mat.vertex_alpha): + vertex_color = create_node( + ShaderNodeVertexColor, "Vertex Color", True, y_offset=0 if abstracted_mat.vertex_color else alpha_y_offset + ) + vertex_color.layer_name = "Col" + if abstracted_mat.vertex_color: # link vertex color to vertex color mul + links.new(vertex_color.outputs[0], vertex_color_mul.inputs[1]) + if abstracted_mat.vertex_alpha: + if put_alpha_into_color: # link vertex color's alpha to vertex alpha mul + links.new(vertex_color.outputs[1], vertex_alpha_mul.inputs[0]) + else: # create vertex alpha node + vertex_alpha = create_node(ShaderNodeVertexColor, "Vertex Alpha", True, y_offset=alpha_y_offset) + vertex_alpha.layer_name = "Alpha" + links.new(vertex_alpha.outputs[0], vertex_alpha_mul.inputs[0]) + + # support for glTF base color which gets multiplied on to textures + mix_rgb = False + if abstracted_mat.texture_sets_col and abstracted_mat.color[:3] != [1.0, 1.0, 1.0]: + print(f"Creating color mul node {abstracted_mat.color} and setting color input") + color_mul = create_node(ShaderNodeMixRGB, "Color Mul") + color_mul.use_clamp, color_mul.blend_type = True, "MULTIPLY" + color_mul.inputs[0].default_value = 1 + color_mul.inputs[1].default_value = abstracted_mat.color + links.new(color_mul.outputs[0], color_input) + color_input = color_mul.inputs[2] + mix_rgb = True + if abstracted_mat.texture_sets_alpha and abstracted_mat.color[3] != 1.0 and abstracted_mat.output_method != "OPA": + print(f"Setting alpha mul node {abstracted_mat.color[3]} and setting alpha input") + alpha_mul = create_node(ShaderNodeMath, "Alpha Mul", y_offset=alpha_y_offset) + alpha_mul.use_clamp, alpha_mul.operation = True, "MULTIPLY" + alpha_mul.inputs[0].default_value = abstracted_mat.color[3] + links.new(alpha_mul.outputs[0], alpha_input) + alpha_input = alpha_mul.inputs[1] + mix_rgb = True + if mix_rgb: + node_x -= 140 + 50 + + uv_map_output = None + if abstracted_mat.textures: # create uv_map + if abstracted_mat.uv_gen: + print("Creating UVmap node") + uv_map_node = create_node(ShaderNodeTexCoord, "UVMap") + uv_map_output = uv_map_node.outputs["Camera"] + else: + print("Creating generated UVmap node (Camera output)") + uv_map_node = create_node(ShaderNodeUVMap, "UVMap") + uv_map_node.uv_map = "UVMap" + uv_map_output = uv_map_node.outputs["UV"] + + tex_color_inputs = [color_input, None] + tex_alpha_inputs = [alpha_input, None] + assert len(abstracted_mat.textures) <= 2, "Too many textures" + if len(abstracted_mat.textures) == 2: + if all(abstracted_tex.set_color for abstracted_tex in abstracted_mat.textures): + print("Creating mix rgb node for multi texture, setting color input") + color_mul = create_node(ShaderNodeMixRGB, "Multitexture Color Mul", True) + color_mul.use_clamp, color_mul.blend_type = True, "MULTIPLY" + color_mul.inputs[0].default_value = 1 + tex_color_inputs = [color_mul.inputs[1], color_mul.inputs[2]] + links.new(color_mul.outputs[0], color_input) + if all(abstracted_tex.set_alpha for abstracted_tex in abstracted_mat.textures): + print("Creating mix rgb node for multi texture, setting alpha input") + alpha_mul = create_node(ShaderNodeMath, "Multitexture Alpha Mul", True, y_offset=alpha_y_offset) + alpha_mul.use_clamp, alpha_mul.operation = True, "MULTIPLY" + tex_alpha_inputs = [alpha_mul.inputs[0], alpha_mul.inputs[1]] + links.new(alpha_mul.outputs[0], alpha_input) + + tex_x_offset = tex_y_offset = 0 + texture_nodes = [] + for abstracted_tex, tex_color_input, tex_alpha_input in zip( + abstracted_mat.textures, tex_color_inputs, tex_alpha_inputs + ): # create invidual texture nodes and link them + tex_node = create_node(ShaderNodeTexImage, "Texture", y_offset=tex_y_offset) + 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) + new_x_offset = -padded_from_node(tex_node) + tex_y_offset -= (tex_node.height * 2) + 125 + + assert uv_map_output + if abstracted_tex.offset != (0.0, 0.0) or abstracted_tex.scale != (1.0, 1.0): + mapping_node = create_node(ShaderNodeMapping, "Mapping", x_offset=new_x_offset, y_offset=tex_y_offset + 98) + mapping_node.vector_type = "POINT" + tex_y_offset -= mapping_node.height + mapping_node.inputs["Location"].default_value = abstracted_tex.offset + (0.0,) + mapping_node.inputs["Scale"].default_value = abstracted_tex.scale + (1.0,) + links.new(uv_map_output, mapping_node.inputs[0]) + links.new(mapping_node.outputs[0], tex_node.inputs[0]) + + new_x_offset -= padded_from_node(mapping_node) + else: + links.new(uv_map_output, tex_node.inputs[0]) + + if abstracted_tex.set_color: + links.new(tex_node.outputs[0], tex_color_input) + if abstracted_tex.set_alpha: + if abstracted_tex.packed_alpha: # i4/i8 + links.new(tex_node.outputs[0], tex_alpha_input) + else: + links.new(tex_node.outputs[1], tex_alpha_input) + + if new_x_offset < tex_x_offset: + tex_x_offset = new_x_offset + node_x += tex_x_offset # update node location + + if abstracted_mat.textures: # update uv_map node location + if len(abstracted_mat.textures) > 1: + node_y += tex_y_offset / len(texture_nodes) + else: + node_y -= 30 + set_location(uv_map_node, True) + + color_input.default_value = abstracted_mat.color[:3] + [1.0] + alpha_input.default_value = abstracted_mat.color[3] + + return new_material + + +def apply_alpha(blender_mesh: Mesh): + color_layer = getColorLayer(blender_mesh, layer="Col") + alpha_layer = getColorLayer(blender_mesh, layer="Alpha") + if not color_layer or not alpha_layer: + return + color = np.empty(len(blender_mesh.loops) * 4, dtype=np.float32) + alpha = np.empty(len(blender_mesh.loops) * 4, dtype=np.float32) + color_layer.foreach_get("color", color) + alpha_layer.foreach_get("color", alpha) + alpha = alpha.reshape(-1, 4) + color = color.reshape(-1, 4) + + # Calculate alpha from the median of the alpha layer RGB + alpha_median = np.median(alpha[:, :3], axis=1) + color[:, 3] = alpha_median + + color = color.flatten() + color_layer.foreach_set("color", color) + + +def find_output_node(material: Material): + if not material.use_nodes: + return None + for node in material.node_tree.nodes: + if isinstance(node, ShaderNodeOutputMaterial) and node.is_active_output: + return node + return None + + +def find_mix_shader_with_transparent(material: Material): + """ + Find mix shader with transparent BSDF, return first one's Fac input and the non transparent BSDF input + """ + output_node = find_output_node(material) + if output_node is None: + return (None, None) + + # check if transparent bsdf is connected + shaders: list[tuple[ShaderNodeMixShader, ShaderNode, ShaderNode]] = [] + for mix_shader in find_linked_nodes(output_node, lambda node: node.bl_idname == "ShaderNodeMixShader"): + transparent, non_transparent = None, None + for inp in mix_shader.inputs: + if inp.name == "Fac" and not inp.links: + continue + link = inp.links[0] + if link.from_node.bl_idname == "ShaderNodeBsdfTransparent": + transparent = link.from_node + else: + non_transparent = link.from_node + if transparent and non_transparent: + shaders.append((mix_shader, transparent, non_transparent)) + + if len(shaders) == 0: + return (None, None) + if len(shaders) > 1: + print(f"WARNING: More than 1 transparent shader connected to a mix shader in {material.name}. Using first one.") + mix_shader, _transparent_bsdf, non_transparent_bsdf = shaders[0] + + return mix_shader, non_transparent_bsdf + + +def find_linked_nodes( + starting_node: ShaderNode | None, + node_check: callable, + specific_input_sockets: list[str] | None = None, + specific_output_sockets: list[str] | None = None, + verbose=False, # small debug feature +): + if starting_node is None: + return [] + nodes: list[ShaderNode] = [] + for inp in starting_node.inputs: + if specific_input_sockets is not None and inp.name not in specific_input_sockets: + continue + if verbose: + print(f"Searching from {inp.name}") + for link in inp.links: + if specific_output_sockets is None or link.from_socket.name in specific_output_sockets: + if verbose: + print(f"Checking {link.from_node.bl_idname} {link.from_socket.name}") + if node_check(link.from_node): + nodes.append(link.from_node) + if verbose: + print("Valid node added, recursive search is skipped") + continue + elif verbose: + print(f"Skipped output socket {link.from_socket.name}") + if verbose: + print(f"Searching recursively in {link.from_node.bl_idname}") + nodes.extend(find_linked_nodes(link.from_node, node_check, specific_output_sockets=specific_output_sockets)) + return list(dict.fromkeys(nodes).keys()) + + +def bsdf_mat_to_abstracted(material: Material): + abstracted_mat = AbstractedN64Material() + + output_node = find_output_node(material) + if output_node is None: + abstracted_mat.color = material.diffuse_color + return abstracted_mat + + mix_shader, color_shader = find_mix_shader_with_transparent(material) + using_mix_shader = mix_shader is not None and color_shader is not None + if using_mix_shader: + alpha_shader, alpha_inp = mix_shader, "Fac" + else: # no transparent mix shader, use the first found shader's inputs for searches + shaders = find_linked_nodes( + output_node, + lambda node: node.bl_idname.startswith("ShaderNodeBsdf") + or node.bl_idname.removeprefix("ShaderNode") + in {"Background", "Emission", "SubsurfaceScattering", "VolumeAbsorption", "VolumeScatter"}, + specific_input_sockets={"Surface"}, + ) + if len(shaders) == 0: + abstracted_mat.color = material.diffuse_color + print(f"WARNING: No shader connected to {material.name}. Using default color.") + return abstracted_mat + if len(shaders) > 1: + print(f"WARNING: More than 1 shader connected to {material.name}. Using first shader.") + color_shader = alpha_shader = shaders[0] + alpha_inp = "Alpha" + if color_shader.bl_idname in {"Background", "Emission"}: # is unlit + abstracted_mat.lighting = False + else: + abstracted_mat.lighting = True + + # set color_inp to Base Color if the input exists, otherwise try Color, if neither work assert + color_inp = next(("Base Color" for inp in color_shader.inputs if inp.name == "Base Color"), None) + if color_inp is None: + color_inp = next(("Color" for inp in color_shader.inputs if inp.name == "Color"), None) + assert color_inp is not None, f"Could not find color input in {material.name}" + + # vertex colors + def get_vtx_layer(nodes): + layer_names = list(dict.fromkeys([node.layer_name for node in nodes]).keys()) + if len(layer_names) > 1: + print(f"WARNING: More than 1 color layer used in {material.name}. Using first layer.") + return layer_names[0] + + vtx_color_nodes = find_linked_nodes( + color_shader, + lambda node: node.bl_idname == "ShaderNodeVertexColor", + specific_input_sockets={color_inp}, + ) + abstracted_mat.vertex_color = get_vtx_layer(vtx_color_nodes) if len(vtx_color_nodes) > 0 else None + # vertex alpha can sometimes be derived from the mean of the color, this is done by the f3d to bsdf converter as well + # because of this, we need to handle both cases + real_vtx_alpha_nodes = find_linked_nodes( + alpha_shader, + lambda node: node.bl_idname == "ShaderNodeVertexColor", + specific_input_sockets={alpha_inp}, + specific_output_sockets={"Alpha"}, + ) + mean_vtx_alpha_nodes = find_linked_nodes( + alpha_shader, + lambda node: node.bl_idname == "ShaderNodeVertexColor", + specific_input_sockets={alpha_inp}, + specific_output_sockets={"Color"}, + ) + if real_vtx_alpha_nodes and mean_vtx_alpha_nodes: + print(f"WARNING: Mixing real and averaged (from color) vertex alpha in {material.name}.") + vtx_alpha_nodes = list(dict.fromkeys(real_vtx_alpha_nodes + mean_vtx_alpha_nodes).keys()) + abstracted_mat.vertex_alpha = get_vtx_layer(vtx_alpha_nodes) if len(vtx_alpha_nodes) > 0 else None + abstracted_mat.alpha_is_median = len(mean_vtx_alpha_nodes) > 0 + + # textures and their respective uv maps and properties like filtering and uvgen + found_uv_map_nodes = [] + alpha_textures = find_linked_nodes( # textures that use alpha as alpha + alpha_shader, + lambda node: node.bl_idname == "ShaderNodeTexImage", + specific_input_sockets={alpha_inp}, + specific_output_sockets={"Alpha"}, + ) + packed_textures = find_linked_nodes( # textures that use color as alpha + alpha_shader, + lambda node: node.bl_idname == "ShaderNodeTexImage", + specific_input_sockets={alpha_inp}, + specific_output_sockets={"Color"}, + ) + color_textures = find_linked_nodes( # textures that use color as color + color_shader, + lambda node: node.bl_idname == "ShaderNodeTexImage", + specific_input_sockets={color_inp}, + specific_output_sockets={"Color"}, + ) + alpha_as_color_textures = find_linked_nodes( # textures that use alpha as color + color_shader, + lambda node: node.bl_idname == "ShaderNodeTexImage", + specific_input_sockets={color_inp}, + specific_output_sockets={"Alpha"}, + ) + textures: list[ShaderNodeTexImage] = list( + dict.fromkeys(color_textures + alpha_as_color_textures + packed_textures + alpha_textures).keys() + ) + if len(textures) > 2: + print(f"WARNING: More than 2 textures connected to {material.name}.") + for tex_node in textures[:2]: + abstracted_tex = AbstractedN64Texture(tex_node.image) + found_uv_map_nodes.extend( + find_linked_nodes( + tex_node, + lambda node: node.bl_idname == "ShaderNodeUVMap", + specific_input_sockets={"Vector"}, + specific_output_sockets={"UV"}, + ) + ) + mapping = find_linked_nodes(tex_node, lambda node: node.bl_idname == "ShaderNodeMapping") + if len(mapping) > 1: + print(f"WARNING: More than 1 mapping node connected to {tex_node.name}.") + elif len(mapping) == 1: + mapping = mapping[0] + abstracted_tex.offset = tuple(mapping.inputs["Location"].default_value) + abstracted_tex.scale = tuple(mapping.inputs["Scale"].default_value) + uv_gen = find_linked_nodes( + tex_node, + lambda node: node.bl_idname == "ShaderNodeTexCoord", + specific_input_sockets={"Vector"}, + specific_output_sockets={"Camera", "Window", "Reflection"}, + ) + if uv_gen: + abstracted_mat.uv_gen = True + if tex_node.interpolation == "Closest": + abstracted_mat.point_filtering = True + abstracted_tex.repeat = tex_node.extension == "REPEAT" + abstracted_tex.set_color = tex_node in color_textures + abstracted_tex.set_alpha = tex_node in alpha_textures + abstracted_tex.packed_alpha = tex_node in packed_textures + abstracted_tex.alpha_as_color = tex_node in alpha_as_color_textures + if abstracted_tex.set_color: + abstracted_mat.texture_sets_col = True + if abstracted_tex.set_alpha: + abstracted_mat.texture_sets_alpha = True + abstracted_mat.textures.append(abstracted_tex) + found_uv_map_names = list(dict.fromkeys([node.uv_map for node in found_uv_map_nodes]).keys()) + if len(found_uv_map_names) > 1: + print(f"WARNING: More than 1 UV map being used in {material.name}. Using first UV map.") + abstracted_mat.uv_map = found_uv_map_names[0] if len(found_uv_map_names) > 0 else "" + + # very simple search for color mul nodes, only really for glTF import support + # (glTF materials can have tex multiplied by base color multiplied vertex colors + lighting!) + + def get_solid_color(nodes): + solid_colors = [] + for node in nodes: + solid_color, non_solid = None, False + for inp in node.inputs: # find all nodes that are multiplied by solid colors but have another input + if not inp.name.startswith("Fac"): + if inp.links: + non_solid = True + else: + solid_color = inp.default_value + if solid_color is not None and non_solid: + solid_colors.append(solid_color) + if len(solid_colors) > 1: + print( + f"WARNING: More than 1 solid color/alpha multiplied by a link node in {material.name}. Using first node." + ) + if len(solid_colors) > 0: + return solid_colors[0] + + solid_color = get_solid_color( + find_linked_nodes( + color_shader, + lambda node: node.bl_idname.startswith("ShaderNodeMix") and node.blend_type == "MULTIPLY", + specific_input_sockets={color_inp}, + ) + ) + if solid_color: + assert hasattr(solid_color, "__iter__"), f"Expected list, got {type(solid_color)}" + abstracted_mat.color = Color(*solid_color[:3], abstracted_mat.color.a) + solid_alpha = get_solid_color( + find_linked_nodes( + alpha_shader, + lambda node: node.bl_idname == "ShaderNodeMath" and node.operation == "MULTIPLY", + specific_input_sockets={alpha_inp}, + ) + ) + if solid_alpha: + assert isinstance(solid_alpha, float | int), f"Expected float, got {type(solid_alpha)}" + abstracted_mat.color.a = solid_alpha + + # get default color shader values given no links + if not color_shader.inputs[color_inp].links: + abstracted_mat.color.r, abstracted_mat.color.g, abstracted_mat.color.b = color_shader.inputs[ + color_inp + ].default_value[:3] + if not alpha_shader.inputs[alpha_inp].links: + abstracted_mat.color.a = alpha_shader.inputs[alpha_inp].default_value + + abstracted_mat.backface_culling = material.use_backface_culling + if bpy.app.version < (4, 2, 0): # before 4.2 we can just use the blend mode + abstracted_mat.output_method = {"CLIP": "CLIP", "BLEND": "XLU"}.get(material.blend_method, "OPA") + elif alpha_textures or abstracted_mat.color.a < 1.0: # otherwise we check if alpha is not 1 or uses a texture + abstracted_mat.output_method = "XLU" + # check if there is a "clip" node connected to alpha + greater_than_nodes = find_linked_nodes( + alpha_shader, + lambda node: node.bl_idname == "ShaderNodeMath" and node.operation == "GREATER_THAN", + specific_input_sockets={alpha_inp}, + ) + if len(greater_than_nodes) > 0: + abstracted_mat.output_method = "CLIP" + return abstracted_mat + + +def material_to_f3d( + obj: Object, + material: Material, + lights_for_colors=False, + default_to_fog=False, + set_rendermode_without_fog=False, +): + print(f"Converting BSDF material {material.name}") + + abstracted_mat = bsdf_mat_to_abstracted(material) + + preset = getDefaultMaterialPreset("Shaded Solid") + new_material = createF3DMat(obj, preset=preset, append=False) + new_material.name = material.name + f3d_mat: F3DMaterialProperty = new_material.f3d_mat + rdp: RDPSettings = f3d_mat.rdp_settings + + if abstracted_mat.color is not None: + f3d_mat.default_light_color = tuple(abstracted_mat.color) + f3d_mat.prim_color = tuple(abstracted_mat.color) + if lights_for_colors: + f3d_mat.set_lights = True + + for i, abstracted_tex in enumerate(abstracted_mat.textures): + f3d_tex: TextureProperty = getattr(f3d_mat, f"tex{i}") + f3d_tex.tex = abstracted_tex.tex + f3d_tex.tex_set = True + f3d_tex.autoprop = abstracted_tex.offset == (0, 0) and abstracted_tex.scale == (1, 1) + s: TextureFieldProperty = f3d_tex.S + t: TextureFieldProperty = f3d_tex.T + s.low = abstracted_tex.offset[0] + t.low = abstracted_tex.offset[1] + s.shift = int(-math.log2(abstracted_tex.scale[0])) + t.shift = int(-math.log2(abstracted_tex.scale[1])) + if abstracted_tex.packed_alpha: # if color is being used as alpha, assume intensity texture + f3d_tex.tex_format = "I8" + + combiner1: CombinerProperty = f3d_mat.combiner1 + combiner2: CombinerProperty = f3d_mat.combiner2 + + def set_combiner_cycle(inputs: list[str], suffix=""): + assert len(inputs) <= 3, f"Too many inputs for combiner cycle: {inputs}" + for inp_attr in ("A", "B", "C", "D"): # default all to 0 + for combiner in (combiner1, combiner2): + setattr(combiner, f"{inp_attr}{suffix}", "0") + if len(inputs) > 2: # if inputs cannot fit into 1 cycle, pass in the result of cycle 2 to A for one more mul + setattr(combiner2, f"A{suffix}", "COMBINED") + else: # if inputs can fit, pass in the result of cycle 1 to d (no mul) + setattr(combiner2, f"D{suffix}", "COMBINED") + if len(inputs) == 0: # if no inputs, set D to 1 + setattr(combiner1, f"D{suffix}", "1") + elif len(inputs) == 1: # if only one input, set D to it (no mul) + setattr(combiner1, f"D{suffix}", inputs[0]) + else: + for i, inp in enumerate(inputs): + if i == 0: + setattr(combiner1, f"A{suffix}", inp) + elif i == 1 or i == 2: + setattr(combiner1 if i == 1 else combiner2, f"C{suffix}", inp) + + # Given an abstracted material we need to create a combiner with some variation, + # to simplify we create a list of every needed input and pass that on to set_combiner_cycle + color_inputs, alpha_inputs = [], [] + for i, abstracted_tex in enumerate(abstracted_mat.textures[:2]): + if abstracted_tex.set_color: + color_inputs.append(f"TEXEL{i}") + if abstracted_tex.set_alpha: + alpha_inputs.append(f"TEXEL{i}") + if abstracted_tex.alpha_as_color: + color_inputs.append(f"TEXEL{i}_ALPHA") + if abstracted_mat.color[:3] != [1, 1, 1]: + if lights_for_colors and abstracted_mat.lighting: + color_inputs.append("SHADE") + else: + color_inputs.append("PRIMITIVE") + if abstracted_mat.color[3] != 1: + alpha_inputs.append("PRIMITIVE") + if (abstracted_mat.lighting or abstracted_mat.vertex_color is not None) and "SHADE" not in color_inputs: + color_inputs.append("SHADE") + if abstracted_mat.vertex_alpha is not None: + alpha_inputs.append("SHADE") + + required_inputs = max(len(color_inputs), len(alpha_inputs)) + if required_inputs > 3: + raise PluginError("Too many inputs for combiner") + set_combiner_cycle(color_inputs) + set_combiner_cycle(alpha_inputs, "_alpha") + + rdp.g_tex_gen = abstracted_mat.uv_gen + rdp.g_packed_normals = ( + bool(abstracted_mat.vertex_color) and abstracted_mat.lighting and isUcodeF3DEX3(bpy.context.scene.f3d_type) + ) + rdp.g_lighting = abstracted_mat.lighting if not bool(abstracted_mat.vertex_color) or rdp.g_packed_normals else False + rdp.g_fog = default_to_fog + rdp.g_cull_back = abstracted_mat.backface_culling + rdp.g_mdsft_text_filt = "G_TF_POINT" if abstracted_mat.point_filtering else "G_TF_BILERP" + use_2cycle = required_inputs > 2 or rdp.g_fog + rdp.g_mdsft_cycletype = "G_CYC_2CYCLE" if use_2cycle else "G_CYC_1CYCLE" + f3d_mat.draw_layer.set_generic_draw_layer(abstracted_mat.output_method) + + main_rendermode = {"OPA": "G_RM_AA_ZB_OPA_SURF", "XLU": "G_RM_AA_ZB_XLU_SURF", "CLIP": "G_RM_AA_ZB_TEX_EDGE"}[ + abstracted_mat.output_method + ] + if use_2cycle: + rdp.rendermode_preset_cycle_1 = "G_RM_FOG_SHADE_A" if rdp.g_fog else "G_RM_PASS" + rdp.rendermode_preset_cycle_2 = main_rendermode + "2" + else: + rdp.rendermode_preset_cycle_1 = main_rendermode + rdp.set_rendermode = set_rendermode_without_fog or rdp.g_fog # sm64 should only set rendermode for fog + + with bpy.context.temp_override(material=new_material): + update_all_node_values(new_material, bpy.context) # Update nodes + + return new_material, abstracted_mat + + +def obj_to_f3d( + obj: Object, + converted_materials: dict[Material, tuple[Material, AbstractedN64Material]], + lights_for_colors=False, + default_to_fog=False, + set_rendermode_without_fog=False, +): + assert obj.type == "MESH" + if not any(mat for mat in obj.data.materials if not is_mat_f3d(mat)): + if obj.data.materials: + return False + else: + preset = getDefaultMaterialPreset("Shaded Solid") + createF3DMat(obj, preset=preset) + return True + print(f"Converting BSDF materials in {obj.name}") + uvs = np.empty((len(obj.data.loops), 2), dtype=np.float32) if len(obj.data.uv_layers) != 1 else None + colors = np.ones((len(obj.data.loops), 4), dtype=np.float32) # TODO: should col and alpha be seperate? + + # populate a dict of material -> list of loop indices + loop_indexes: dict[Material, list] = {} + for poly in obj.data.polygons: + material = obj.data.materials[poly.material_index] if poly.material_index < len(obj.data.materials) else None + if material is None: + continue + if material not in loop_indexes: + loop_indexes[material] = [] + for loop_idx in poly.loop_indices: + loop_indexes[material].append(loop_idx) + + def get_layer_and_convert(layer_name: str | None): # get color layer, convert it if needed + layer = obj.data.vertex_colors.get(layer_name or "", obj.data.vertex_colors.active) + if layer is None: + return None + layer_name = layer.name + convertColorAttribute(obj.data, layer_name) + return obj.data.vertex_colors[layer_name] # HACK: layer cannot be trusted + + 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 not in converted_materials: + converted_materials[material] = material_to_f3d( + obj, material, lights_for_colors, default_to_fog, set_rendermode_without_fog + ) + new_material, abstracted_mat = converted_materials[material] + obj.material_slots[index].material = new_material + + if uvs is not None: # apply the used uv or fallback on active + uv_map_layer = obj.data.uv_layers.get(abstracted_mat.uv_map or "", obj.data.uv_layers.active) + print(f"Updating main UV map with {uv_map_layer.name} UVs from {material.name}.") + if uv_map_layer is not None: + for loop_index in loop_indexes[material]: + uvs[loop_index] = uv_map_layer.data[loop_index].uv + + # apply the used color/alpha or fallback on active + col_layer = get_layer_and_convert(abstracted_mat.vertex_color) + if col_layer is not None: + for loop_idx in loop_indexes[material]: + colors[loop_idx, :3] = col_layer.data[loop_idx].color[:3] + alpha_layer = get_layer_and_convert(abstracted_mat.vertex_alpha) + if alpha_layer is not None: + if abstracted_mat.alpha_is_median: + for loop_idx in loop_indexes[material]: + colors[loop_idx, 3] = colorToLuminance(alpha_layer.data[loop_idx].color) + else: + for loop_idx in loop_indexes[material]: + colors[loop_idx, 3] = alpha_layer.data[loop_idx].color[3] + + if uvs is not None: # If there wasn´t exactly one UV map, we need to create one singular UV map + while len(obj.data.uv_layers.values()) > 0: + obj.data.uv_layers.remove(obj.data.uv_layers.values()[0]) + obj.data.uv_layers.new(name="UVMap") + obj.data.uv_layers.active = obj.data.uv_layers["UVMap"] + obj.data.uv_layers["UVMap"].data.foreach_set("uv", uvs.flatten()) + + while len(obj.data.vertex_colors) > 0: # remove all existing colors + obj.data.vertex_colors.remove(obj.data.vertex_colors[0]) + # get the alpha as rgb, then flatten it + alpha_layer = obj.data.color_attributes.new("Alpha", "FLOAT_COLOR", "CORNER") + alpha_layer.data.foreach_set("color", np.repeat(colors[:, 3][:, np.newaxis], 4, axis=1).flatten()) + # set the alpha to 1 for the color layer + color_layer = obj.data.color_attributes.new("Col", "FLOAT_COLOR", "CORNER") + colors[:, 3] = 1 + color_layer.data.foreach_set("color", colors.flatten()) + return True + + +def obj_to_bsdf(obj: Object, converted_materials: dict[Material, Material], put_alpha_into_color: bool): + assert obj.type == "MESH" + print(f"Converting F3D materials in {obj.name}") + if not any(mat for mat in obj.data.materials if is_mat_f3d(mat)): + return False + if put_alpha_into_color: + apply_alpha(obj.data) + 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 converted_materials: + obj.material_slots[index].material = converted_materials[material] + else: + obj.material_slots[index].material = material_to_bsdf(material, put_alpha_into_color) + return True diff --git a/fast64_internal/f3d/bsdf_converter/operators.py b/fast64_internal/f3d/bsdf_converter/operators.py new file mode 100644 index 000000000..57cbc5fc2 --- /dev/null +++ b/fast64_internal/f3d/bsdf_converter/operators.py @@ -0,0 +1,155 @@ +import copy + +import bpy +from bpy.utils import register_class, unregister_class +from bpy.props import EnumProperty, BoolProperty +from bpy.types import Context, Object, Material, UILayout + +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"), ("All", "All", "All")] +RECOGNISED_GAMEMODES = ["SM64", "OOT", "MK64"] + + +def draw_generic_converter_props(owner, layout: UILayout, direction: str, context: Context): + if direction == "": + layout.prop(owner, "converter_type") + layout.prop(owner, "backup") + if direction == "BSDF": + layout.prop(owner, "put_alpha_into_color") + elif direction == "F3D": + recognised_gamemode = context.scene.gameEditorMode in RECOGNISED_GAMEMODES + if recognised_gamemode: + layout.prop(owner, "use_recommended") + if not owner.use_recommended or not recognised_gamemode: + layout.prop(owner, "lights_for_colors") + layout.prop(owner, "default_to_fog") + layout.prop(owner, "set_rendermode_without_fog") + + +class F3D_ConvertBSDF(OperatorBase): + bl_idname = "scene.f3d_convert_to_bsdf" + bl_label = "BSDF Converter (F3D To BSDF or BSDF To F3D)" + bl_options = {"REGISTER", "UNDO", "PRESET"} + icon = "MATERIAL" + + # we store these in the operator itself for user presets! + direction: EnumProperty(items=[("F3D", "BSDF To F3D", "F3D"), ("BSDF", "F3D To BSDF", "BSDF")], name="Direction") + converter_type: EnumProperty(items=converter_enum, name="Type") + backup: BoolProperty(default=True, name="Backup") + put_alpha_into_color: BoolProperty(default=False, name="Put Alpha Into Color") + use_recommended: BoolProperty(default=True, name="Use Recommended For Current Gamemode") + lights_for_colors: BoolProperty(default=False, name="Lights For Colors") + default_to_fog: BoolProperty(default=False, name="Default To Fog") + set_rendermode_without_fog: BoolProperty(default=False, name="Set RenderMode Even Without Fog") + + def draw(self, context: Context): + layout = self.layout.column() + layout.prop(self, "direction") + draw_generic_converter_props(self, layout, self.direction, context) + + def execute_operator(self, context: Context): + collection = context.scene.collection + view_layer = context.view_layer + scene = context.scene + + def exclude_non_mesh(objs: list[Object]) -> list[Object]: + return [obj for obj in objs if obj.type == "MESH"] + + if self.converter_type == "Object": + objs = exclude_non_mesh(context.selected_objects) + if not objs: + raise PluginError("No objects selected to convert.") + elif self.converter_type == "Scene": + objs = exclude_non_mesh(scene.objects) + if not objs: + raise PluginError("No objects in current scene to convert.") + elif self.converter_type == "All": + objs = exclude_non_mesh(bpy.data.objects) + if not objs: + raise PluginError("No objects in current file to convert.") + + if self.use_recommended and scene.gameEditorMode in RECOGNISED_GAMEMODES: + game_mode: str = scene.gameEditorMode + lights_for_colors = game_mode == "SM64" + default_to_fog = game_mode != "SM64" + set_rendermode_without_fog = default_to_fog + else: + lights_for_colors, default_to_fog, set_rendermode_without_fog = ( + self.lights_for_colors, + self.default_to_fog, + self.set_rendermode_without_fog, + ) + original_names = [obj.name for obj in objs] + new_objs: list[Object] = [] + backup_collection = None + + try: + materials: dict[Material, Material] = {} + converted_something = False + for old_obj in objs: # make copies and convert them + obj = old_obj.copy() + obj.data = old_obj.data.copy() + scene.collection.objects.link(obj) + view_layer.objects.active = obj + new_objs.append(obj) + if self.direction == "F3D": + converted_something |= obj_to_f3d( + obj, materials, lights_for_colors, default_to_fog, set_rendermode_without_fog + ) + elif self.direction == "BSDF": + converted_something |= obj_to_bsdf(obj, materials, self.put_alpha_into_color) + if not converted_something: # nothing converted + raise PluginError("No materials to convert.") + + bpy.ops.object.select_all(action="DESELECT") + if self.backup: + name = "BSDF -> F3D Backup" if self.direction == "F3D" else "F3D -> BSDF Backup" + if name in bpy.data.collections: + backup_collection = bpy.data.collections[name] + else: + backup_collection = bpy.data.collections.new(name) + 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.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_ConvertBSDF,) + + +def bsdf_converter_ops_register(): + for cls in classes: + register_class(cls) + + +def bsdf_converter_ops_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/f3d/bsdf_converter/properties.py b/fast64_internal/f3d/bsdf_converter/properties.py new file mode 100644 index 000000000..fa8c4dbb1 --- /dev/null +++ b/fast64_internal/f3d/bsdf_converter/properties.py @@ -0,0 +1,31 @@ +from bpy.utils import register_class, unregister_class +from bpy.types import PropertyGroup +from bpy.props import EnumProperty, BoolProperty + +from .operators import converter_enum + +class F3D_BSDFConverterProperties(PropertyGroup): + """ + Properties in scene.fast64.f3d.bsdf_converter + """ + + backup: BoolProperty(default=True, name="Backup") + converter_type: EnumProperty(items=converter_enum, name="Type") + put_alpha_into_color: BoolProperty(default=False, name="Put Alpha Into Color") + use_recommended: BoolProperty(default=True, name="Use Recommended For Current Gamemode") + lights_for_colors: BoolProperty(default=False, name="Lights For Colors") + default_to_fog: BoolProperty(default=False, name="Default To Fog") + set_rendermode_without_fog: BoolProperty(default=False, name="Set RenderMode Even Without Fog") + + +classes = (F3D_BSDFConverterProperties,) + + +def bsdf_converter_props_register(): + for cls in classes: + register_class(cls) + + +def bsdf_converter_props_unregister(): + for cls in reversed(classes): + unregister_class(cls) diff --git a/fast64_internal/f3d/bsdf_converter/ui.py b/fast64_internal/f3d/bsdf_converter/ui.py new file mode 100644 index 000000000..26e5dc80e --- /dev/null +++ b/fast64_internal/f3d/bsdf_converter/ui.py @@ -0,0 +1,29 @@ +"""Usually this would be panel.py but there is no actual panel since we only draw in the tools panel.""" + +from bpy.types import UILayout, Context + +from .operators import F3D_ConvertBSDF, draw_generic_converter_props +from .properties import F3D_BSDFConverterProperties + + +def bsdf_converter_panel_draw(layout: UILayout, context: Context): + col = layout.column() + bsdf_converter: F3D_BSDFConverterProperties = context.scene.fast64.f3d.bsdf_converter + draw_generic_converter_props(bsdf_converter, col, "", context) + + for direction in ("F3D", "BSDF"): + box = layout.box().column() + draw_generic_converter_props(bsdf_converter, box, direction, context) + opposite = "BSDF" if direction == "F3D" else "F3D" + F3D_ConvertBSDF.draw_props( + box, + text=f"Convert {opposite} to {direction}", + direction=direction, + converter_type=bsdf_converter.converter_type, + backup=bsdf_converter.backup, + put_alpha_into_color=bsdf_converter.put_alpha_into_color, + use_recommended=bsdf_converter.use_recommended, + lights_for_colors=bsdf_converter.lights_for_colors, + default_to_fog=bsdf_converter.default_to_fog, + set_rendermode_without_fog=bsdf_converter.set_rendermode_without_fog, + ) diff --git a/fast64_internal/f3d/f3d_material.py b/fast64_internal/f3d/f3d_material.py index 1f0970553..ac5d1536d 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,27 +322,36 @@ 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) + def set_generic_draw_layer(self, output_method: str): + if output_method == "CLIP": + self.sm64, self.oot = "4", "Opaque" + elif output_method == "XLU": + self.sm64, self.oot = "5", "Transparent" + else: + self.sm64, self.oot = "1", "Opaque" + def key(self): return (self.sm64, self.oot) @@ -2565,7 +2581,7 @@ def add_f3d_mat_to_obj(obj: bpy.types.Object, material, index=None): bpy.context.object.active_material_index = index -def createF3DMat(obj: Object | None, preset="Shaded Solid", index=None): +def createF3DMat(obj: Object | None, preset="Shaded Solid", index=None, append=True): # link all node_groups + material from addon's data .blend link_f3d_material_library() @@ -2579,7 +2595,8 @@ def createF3DMat(obj: Object | None, preset="Shaded Solid", index=None): createScenePropertiesForMaterial(material) - add_f3d_mat_to_obj(obj, material, index) + if append: + add_f3d_mat_to_obj(obj, material, index) material.is_f3d = True material.mat_ver = F3D_MAT_CUR_VERSION @@ -2865,9 +2882,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): diff --git a/fast64_internal/f3d_material_converter.py b/fast64_internal/f3d_material_converter.py index fffb903da..744ca79b3 100644 --- a/fast64_internal/f3d_material_converter.py +++ b/fast64_internal/f3d_material_converter.py @@ -1,7 +1,9 @@ # This is not in the f3d package since copying materials requires copying collision settings from all games as well. import bpy +from bpy.types import UILayout, Context from bpy.utils import register_class, unregister_class + from .f3d.f3d_material import * from .f3d.f3d_material_helpers import node_tree_copy from .utility import * @@ -164,105 +166,12 @@ def convertF3DtoNewVersion( traceback.print_exc() -def convertAllBSDFtoF3D(objs, renameUV): - # Dict of non-f3d materials : converted f3d materials - # handles cases where materials are used in multiple objects - materialDict = {} - for obj in objs: - if renameUV: - for uv_layer in obj.data.uv_layers: - uv_layer.name = "UVMap" - for index in range(len(obj.material_slots)): - material = obj.material_slots[index].material - if material is not None and not material.is_f3d: - if material in materialDict: - print("Existing material") - obj.material_slots[index].material = materialDict[material] - else: - print("New material") - convertBSDFtoF3D(obj, index, material, materialDict) - - -def convertBSDFtoF3D(obj, index, material, materialDict): - if not material.use_nodes: - newMaterial = createF3DMat(obj, preset="Shaded Solid", index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.default_light_color = material.diffuse_color - updateMatWithName(newMaterial, material, materialDict) - - elif "Principled BSDF" in material.node_tree.nodes: - tex0Node = material.node_tree.nodes["Principled BSDF"].inputs["Base Color"] - if len(tex0Node.links) == 0: - newMaterial = createF3DMat(obj, preset=getDefaultMaterialPreset("Shaded Solid"), index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.default_light_color = tex0Node.default_value - updateMatWithName(newMaterial, material, materialDict) - else: - if isinstance(tex0Node.links[0].from_node, bpy.types.ShaderNodeTexImage): - if "convert_preset" in material: - presetName = material["convert_preset"] - if presetName not in [enumValue[0] for enumValue in enumMaterialPresets]: - raise PluginError( - "During BSDF to F3D conversion, for material '" - + material.name - + "'," - + " enum '" - + presetName - + "' was not found in material preset enum list." - ) - else: - presetName = getDefaultMaterialPreset("Shaded Texture") - newMaterial = createF3DMat(obj, preset=presetName, index=index) - f3dMat = newMaterial.f3d_mat if newMaterial.mat_ver > 3 else newMaterial - f3dMat.tex0.tex = tex0Node.links[0].from_node.image - updateMatWithName(newMaterial, material, materialDict) - else: - print("Principled BSDF material does not have an Image Node attached to its Base Color.") - else: - print("Material is not a Principled BSDF or non-node material.") - - def updateMatWithName(f3dMat, oldMat, materialDict): f3dMat.name = oldMat.name + "_f3d" update_preset_manual(f3dMat, bpy.context) materialDict[oldMat] = f3dMat -class BSDFConvert(bpy.types.Operator): - # set bl_ properties - bl_idname = "object.convert_bsdf" - bl_label = "Principled BSDF to F3D Converter" - bl_options = {"REGISTER", "UNDO", "PRESET"} - - # Called on demand (i.e. button press, menu item) - # Can also be called from operator search menu (Spacebar) - def execute(self, context): - try: - if context.mode != "OBJECT": - raise PluginError("Operator can only be used in object mode.") - - if context.scene.bsdf_conv_all: - convertAllBSDFtoF3D( - [obj for obj in bpy.data.objects if obj.type == "MESH"], - context.scene.rename_uv_maps, - ) - else: - if len(context.selected_objects) == 0: - raise PluginError("Mesh not selected.") - elif type(context.selected_objects[0].data) is not bpy.types.Mesh: - raise PluginError("Mesh not selected.") - - obj = context.selected_objects[0] - convertAllBSDFtoF3D([obj], context.scene.rename_uv_maps) - - except Exception as e: - raisePluginError(self, e) - return {"CANCELLED"} - - self.report({"INFO"}, "Created F3D material.") - return {"FINISHED"} # must return a set - - class MatUpdateConvert(bpy.types.Operator): # set bl_ properties bl_idname = "object.convert_f3d_update" @@ -297,62 +206,27 @@ def execute(self, context): return {"FINISHED"} # must return a set -class F3DMaterialConverterPanel(bpy.types.Panel): - bl_label = "F3D Material Converter" - bl_idname = "MATERIAL_PT_F3D_Material_Converter" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "Fast64" - - @classmethod - def poll(cls, context): - return True - # return hasattr(context, 'object') and context.object is not None and \ - # isinstance(context.object.data, bpy.types.Mesh) - - def draw(self, context): - # mesh = context.object.data - self.layout.operator(BSDFConvert.bl_idname) - self.layout.prop(context.scene, "bsdf_conv_all") - self.layout.prop(context.scene, "rename_uv_maps") - op = self.layout.operator(MatUpdateConvert.bl_idname) - op.update_conv_all = context.scene.update_conv_all - self.layout.prop(context.scene, "update_conv_all") - self.layout.operator(ReloadDefaultF3DPresets.bl_idname) - - -bsdf_conv_classes = ( - BSDFConvert, - MatUpdateConvert, -) +def mat_updater_draw(layout: UILayout, context: Context): + col = layout.column() + op = col.operator(MatUpdateConvert.bl_idname) + op.update_conv_all = context.scene.update_conv_all + col.prop(context.scene, "update_conv_all") + col.operator(ReloadDefaultF3DPresets.bl_idname) -bsdf_conv_panel_classes = (F3DMaterialConverterPanel,) - -def bsdf_conv_panel_regsiter(): - for cls in bsdf_conv_panel_classes: - register_class(cls) - - -def bsdf_conv_panel_unregsiter(): - for cls in bsdf_conv_panel_classes: - unregister_class(cls) +mat_updater_classes = (MatUpdateConvert,) -def bsdf_conv_register(): - for cls in bsdf_conv_classes: +def mat_updater_register(): + for cls in mat_updater_classes: register_class(cls) # Moved to Level Root - bpy.types.Scene.bsdf_conv_all = bpy.props.BoolProperty(name="Convert all objects", default=True) bpy.types.Scene.update_conv_all = bpy.props.BoolProperty(name="Convert all objects", default=True) - bpy.types.Scene.rename_uv_maps = bpy.props.BoolProperty(name="Rename UV maps", default=True) -def bsdf_conv_unregister(): - for cls in bsdf_conv_classes: +def mat_updater_unregister(): + for cls in mat_updater_classes: unregister_class(cls) - del bpy.types.Scene.bsdf_conv_all del bpy.types.Scene.update_conv_all - del bpy.types.Scene.rename_uv_maps