Files
Lila 3a7fcc232e [SM64] Add lighting engine presets (#531)
* [SM64] Add lighting engine presets

* remove preset dir function
2025-08-15 14:18:41 -04:00

5082 lines
198 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
import bpy, math, os
from bpy.types import (
Attribute,
Context,
Event,
Image,
Light,
Material,
Menu,
Mesh,
NodeGroupOutput,
NodeInputs,
NodeLink,
NodeSocket,
NodeTree,
Object,
Operator,
Panel,
Property,
PropertyGroup,
Scene,
ShaderNodeGroup,
TextureNodeImage,
UILayout,
VIEW3D_HT_header,
World,
)
from bl_operators.presets import AddPresetBase
from bpy.utils import register_class, unregister_class
from mathutils import Color
from .f3d_enums import *
from .f3d_gbi import (
get_F3D_GBI,
enumTexScroll,
isUcodeF3DEX1,
isUcodeF3DEX3,
is_ucode_f3d,
is_ucode_t3d,
default_draw_layers,
)
from .f3d_material_presets import *
from ..utility import *
from ..render_settings import (
Fast64RenderSettings_Properties,
update_scene_props_from_render_settings,
ManualUpdatePreviewOperator,
)
from .f3d_material_helpers import F3DMaterial_UpdateLock, node_tree_copy
from bpy.app.handlers import persistent
from typing import Generator, Optional, Tuple, Any, Dict, Union
F3D_MAT_CUR_VERSION = 6 # Increment this when changing the nodes
F3DMaterialHash = Any # giant tuple
logging.basicConfig(format="%(asctime)s: %(message)s", datefmt="%m/%d/%Y %I:%M:%S %p")
logger = logging.getLogger(__name__)
bitSizeDict = {
"G_IM_SIZ_4b": 4,
"G_IM_SIZ_8b": 8,
"G_IM_SIZ_16b": 16,
"G_IM_SIZ_32b": 32,
}
texBitSizeF3D = {
"I4": "G_IM_SIZ_4b",
"IA4": "G_IM_SIZ_4b",
"CI4": "G_IM_SIZ_4b",
"I8": "G_IM_SIZ_8b",
"IA8": "G_IM_SIZ_8b",
"CI8": "G_IM_SIZ_8b",
"RGBA16": "G_IM_SIZ_16b",
"IA16": "G_IM_SIZ_16b",
"YUV16": "G_IM_SIZ_16b",
"RGBA32": "G_IM_SIZ_32b",
}
texFormatOf = {
"I4": "G_IM_FMT_I",
"IA4": "G_IM_FMT_IA",
"CI4": "G_IM_FMT_CI",
"I8": "G_IM_FMT_I",
"IA8": "G_IM_FMT_IA",
"CI8": "G_IM_FMT_CI",
"RGBA16": "G_IM_FMT_RGBA",
"IA16": "G_IM_FMT_IA",
"YUV16": "G_IM_FMT_YUV",
"RGBA32": "G_IM_FMT_RGBA",
}
sm64EnumDrawLayers = [
("0", "Background (0x00)", "Background"),
("1", "Opaque (0x01)", "Opaque"),
("2", "Opaque Decal (0x02)", "Opaque Decal"),
("3", "Opaque Intersecting (0x03)", "Opaque Intersecting"),
("4", "Cutout (0x04)", "Cutout"),
("5", "Transparent (0x05)", "Transparent"),
("6", "Transparent Decal (0x06)", "Transparent Decal"),
("7", "Transparent Intersecting (0x07)", "Transparent Intersecting"),
]
ootEnumDrawLayers = [
("Opaque", "Opaque", "Opaque"),
("Transparent", "Transparent", "Transparent"),
("Overlay", "Overlay", "Overlay"),
]
drawLayerSM64toOOT = {
"0": "Opaque",
"1": "Opaque",
"2": "Opaque",
"3": "Opaque",
"4": "Opaque",
"5": "Transparent",
"6": "Transparent",
"7": "Transparent",
}
drawLayerOOTtoSM64 = {
"Opaque": "1",
"Transparent": "5",
"Overlay": "1",
}
def menu_items_enum(_self, context):
items = ["Combiner", "Sources"]
if len(geo_modes_in_ucode(context.scene.f3d_type)) > 1:
items.append("Geo")
items.extend(["Upper", "Lower"])
return [(item, item, item) for item in items]
enumF3DSource = [
("None", "None", "None"),
("Texture", "Texture", "Texture"),
("Tile Size", "Tile Size", "Tile Size"),
("Primitive", "Primitive", "Primitive"),
("Environment", "Environment", "Environment"),
("Shade", "Shade", "Shade"),
("Key", "Key", "Key"),
("LOD Fraction", "LOD Fraction", "LOD Fraction"),
("Convert", "Convert", "Convert"),
]
defaultMaterialPresets = {
"Shaded Solid": {"SM64": "Shaded Solid", "OOT": "oot_shaded_solid"},
"Shaded Texture": {"SM64": "Shaded Texture", "OOT": "oot_shaded_texture"},
}
F3D_GEO_MODES = {
"zBuffer": "g_zbuffer",
"shade": "g_shade",
"cullFront": "g_cull_front",
"cullBack": "g_cull_back",
"fog": "g_fog",
"lighting": "g_lighting",
"texGen": "g_tex_gen",
"texGenLinear": "g_tex_gen_linear",
"lod": "g_lod",
"shadeSmooth": "g_shade_smooth",
}
F3DLX_GEO_MODES = {
"clipping": "g_clipping",
}
F3DEX3_GEO_MODES = {
"ambientOcclusion": "g_ambocclusion",
"attroffsetZ": "g_attroffset_z_enable",
"attroffsetST": "g_attroffset_st_enable",
"packedNormals": "g_packed_normals",
"lightToAlpha": "g_lighttoalpha",
"specularLighting": "g_lighting_specular",
"fresnelToColor": "g_fresnel_color",
"fresnelToAlpha": "g_fresnel_alpha",
}
T3D_GEO_MODES = {
"cullFront": "g_cull_front",
"cullBack": "g_cull_back",
"fog": "g_fog",
"texGen": "g_tex_gen",
}
def geo_modes_in_ucode(UCODE_VER: str):
geo_modes = {}
if is_ucode_f3d(UCODE_VER):
geo_modes.update(F3D_GEO_MODES)
if isUcodeF3DEX1(UCODE_VER):
geo_modes.update(F3DLX_GEO_MODES)
if isUcodeF3DEX3(UCODE_VER):
geo_modes.update(F3DEX3_GEO_MODES)
if is_ucode_t3d(UCODE_VER):
geo_modes.update(T3D_GEO_MODES)
return geo_modes
def sources_in_ucode(UCODE_VER: str):
sources = ["Primitive", "Environment", "Key", "Convert", "Fog"]
if not is_ucode_t3d(UCODE_VER) or is_ucode_f3d(UCODE_VER):
sources.extend(["Lighting", "Clip Ratio"])
if isUcodeF3DEX3(UCODE_VER):
sources.extend(["AO", "Fresnel", "ST Attr Offset", "Z Attr Offset"])
return sources
def inherit_light_and_fog():
return is_ucode_t3d(bpy.context.scene.f3d_type)
def getDefaultMaterialPreset(category):
game = bpy.context.scene.gameEditorMode
if game in defaultMaterialPresets[category]:
return defaultMaterialPresets[category][game]
else:
return "Shaded Solid"
def update_draw_layer(self, context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
drawLayer = material.f3d_mat.draw_layer
if context.scene.gameEditorMode == "SM64":
drawLayer.oot = drawLayerSM64toOOT[drawLayer.sm64]
elif context.scene.gameEditorMode in {"OOT", "MM"}:
if material.f3d_mat.draw_layer.oot == "Opaque":
if int(material.f3d_mat.draw_layer.sm64) > 4:
material.f3d_mat.draw_layer.sm64 = "1"
elif material.f3d_mat.draw_layer.oot == "Transparent":
if int(material.f3d_mat.draw_layer.sm64) < 5:
material.f3d_mat.draw_layer.sm64 = "5"
material.f3d_mat.presetName = "Custom"
update_blend_method(material, context)
set_output_node_groups(material)
def get_world_layer_defaults(scene, game_mode: str, layer: str):
world = scene.world
if world is None:
return default_draw_layers.get(game_mode, {}).get(layer, ("", ""))
if game_mode == "SM64":
return (
getattr(world, f"draw_layer_{layer}_cycle_1", ""),
getattr(world, f"draw_layer_{layer}_cycle_2", ""),
)
elif game_mode == "OOT":
return (
getattr(world.ootDefaultRenderModes, f"{layer.lower()}Cycle1", ""),
getattr(world.ootDefaultRenderModes, f"{layer.lower()}Cycle2", ""),
)
else:
assert (
False
), f"game_mode={game_mode} has no draw layer defaults, this function should not have been called at all with it"
def rendermode_preset_to_advanced(material: bpy.types.Material):
"""
Set all individual controls for the rendermode from the preset rendermode.
"""
scene = bpy.context.scene
f3d_mat = material.f3d_mat
settings = f3d_mat.rdp_settings
f3d = get_F3D_GBI()
if settings.rendermode_advanced_enabled and settings.set_rendermode:
# Rendermode is being set by the material and in advanced mode, don't overwrite any settings
return
cycle_1, cycle_2 = settings.rendermode_preset_cycle_1, settings.rendermode_preset_cycle_2
if not settings.set_rendermode:
game_mode = scene.gameEditorMode
layer = getattr(f3d_mat.draw_layer, game_mode.lower(), None)
if layer is None: # Game mode has no layer, don´t change anything
return
possible_cycle_1, possible_cycle_2 = get_world_layer_defaults(scene, game_mode, layer)
if getattr(f3d, possible_cycle_1, None) is not None and getattr(f3d, possible_cycle_2, None) is not None:
cycle_1, cycle_2 = possible_cycle_1, possible_cycle_2
# Some presets are not implemented in the blender enum, so print a warning and turn on advanced
try:
settings.rendermode_preset_cycle_1, settings.rendermode_preset_cycle_2 = cycle_1, cycle_2
settings.rendermode_advanced_enabled = False
except TypeError as exc:
print(
f"Render mode presets {cycle_1} or {cycle_2} probably not included in render mode preset enum:\n{exc}",
)
settings.rendermode_advanced_enabled = True
def get_with_default(preset, default):
# Use the material's settings even if we are not setting rendermode.
# This allows the user to enable setting rendermode, set it up as they
# want, then disable it, and have it still previewed that way.
return getattr(f3d, preset, default)
is_two_cycle = settings.g_mdsft_cycletype == "G_CYC_2CYCLE"
if is_two_cycle:
r1 = get_with_default(cycle_1, f3d.G_RM_FOG_SHADE_A)
r2 = get_with_default(cycle_2, f3d.G_RM_AA_ZB_OPA_SURF2)
r = r1 | r2
else:
r = get_with_default(cycle_1, f3d.G_RM_AA_ZB_OPA_SURF)
r1 = r
# The cycle 1 bits are copied to the cycle 2 bits at export if in 1-cycle mode
# (the hardware requires them to be the same). So, here we also move the cycle 1
# bits to the cycle 2 slots. r2 is only read for the cycle dependent settings below.
r2 = r >> 2
# cycle independent
settings.aa_en = (r & f3d.AA_EN) != 0
settings.z_cmp = (r & f3d.Z_CMP) != 0
settings.z_upd = (r & f3d.Z_UPD) != 0
settings.im_rd = (r & f3d.IM_RD) != 0
settings.clr_on_cvg = (r & f3d.CLR_ON_CVG) != 0
settings.cvg_dst = f3d.cvgDstDict[r & f3d.CVG_DST_SAVE]
settings.zmode = f3d.zmodeDict[r & f3d.ZMODE_DEC]
settings.cvg_x_alpha = (r & f3d.CVG_X_ALPHA) != 0
settings.alpha_cvg_sel = (r & f3d.ALPHA_CVG_SEL) != 0
settings.force_bl = (r & f3d.FORCE_BL) != 0
# cycle dependent / lerp
settings.blend_p1 = f3d.blendColorDict[(r1 >> 30) & 3]
settings.blend_p2 = f3d.blendColorDict[(r2 >> 28) & 3]
settings.blend_a1 = f3d.blendAlphaDict[(r1 >> 26) & 3]
settings.blend_a2 = f3d.blendAlphaDict[(r2 >> 24) & 3]
settings.blend_m1 = f3d.blendColorDict[(r1 >> 22) & 3]
settings.blend_m2 = f3d.blendColorDict[(r2 >> 20) & 3]
settings.blend_b1 = f3d.blendMixDict[(r1 >> 18) & 3]
settings.blend_b2 = f3d.blendMixDict[(r2 >> 16) & 3]
def does_blender_use_mix(settings: "RDPSettings", mix: str, default_for_no_rendermode: bool = False) -> bool:
if not settings.set_rendermode:
return default_for_no_rendermode
is_two_cycle = settings.g_mdsft_cycletype == "G_CYC_2CYCLE"
return settings.blend_b1 == mix or (is_two_cycle and settings.blend_b2 == mix)
def is_blender_equation_equal(settings: "RDPSettings", cycle: int, p: str, a: str, m: str, b: str) -> bool:
assert cycle in {1, 2, -1} # -1 = last cycle
if cycle == -1:
cycle = 2 if settings.g_mdsft_cycletype == "G_CYC_2CYCLE" else 1
return (
getattr(settings, f"blend_p{cycle}") == p
and getattr(settings, f"blend_a{cycle}") == a
and getattr(settings, f"blend_m{cycle}") == m
and getattr(settings, f"blend_b{cycle}") == b
)
def is_blender_doing_fog(settings: "RDPSettings") -> bool:
return is_blender_equation_equal(
settings,
# If 2 cycle, fog must be in first cycle.
1,
"G_BL_CLR_FOG",
"G_BL_A_SHADE",
# While technically it being fog only requires that P and A be fog color
# and shade alpha, the only reasonable choice for M and B in this case
# is color in and 1-A.
"G_BL_CLR_IN",
"G_BL_1MA",
)
def get_output_method(material: bpy.types.Material) -> str:
rendermode_preset_to_advanced(material) # Make sure advanced settings are updated
settings = material.f3d_mat.rdp_settings
if settings.cvg_x_alpha:
return "CLIP"
if settings.force_bl and is_blender_equation_equal(
settings, -1, "G_BL_CLR_IN", "G_BL_A_IN", "G_BL_CLR_MEM", "G_BL_1MA"
):
return "XLU"
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"
if bpy.app.version >= (4, 2, 0):
if blend_mode == "CLIP":
material.surface_render_method = "DITHERED"
else:
material.surface_render_method = "BLENDED"
elif blend_mode == "OPA":
material.blend_method = "OPAQUE"
elif blend_mode == "CLIP":
material.blend_method = "CLIP"
elif blend_mode in {"XLU", "DECAL"}:
material.blend_method = "BLEND"
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 key(self):
return (self.sm64, self.oot)
def getTmemWordUsage(texFormat, width, height):
texelsPerWord = 64 // texBitSizeInt[texFormat]
return (width + texelsPerWord - 1) // texelsPerWord * height
def getTmemMax(texFormat):
return 4096 if texFormat[:2] != "CI" else 2048
# Necessary for UV half pixel offset (see 13.7.5.3)
def isTexturePointSampled(material):
f3dMat = material.f3d_mat
return f3dMat.rdp_settings.g_mdsft_text_filt == "G_TF_POINT"
def F3DOrganizeLights(self, context):
# Flag to prevent infinite recursion on update callback
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
lightList = []
if self.f3d_light1 is not None:
lightList.append(self.f3d_light1)
if self.f3d_light2 is not None:
lightList.append(self.f3d_light2)
if self.f3d_light3 is not None:
lightList.append(self.f3d_light3)
if self.f3d_light4 is not None:
lightList.append(self.f3d_light4)
if self.f3d_light5 is not None:
lightList.append(self.f3d_light5)
if self.f3d_light5 is not None:
lightList.append(self.f3d_light6)
if self.f3d_light6 is not None:
lightList.append(self.f3d_light7)
self.f3d_light1 = lightList[0] if len(lightList) > 0 else None
self.f3d_light2 = lightList[1] if len(lightList) > 1 else None
self.f3d_light3 = lightList[2] if len(lightList) > 2 else None
self.f3d_light4 = lightList[3] if len(lightList) > 3 else None
self.f3d_light5 = lightList[4] if len(lightList) > 4 else None
self.f3d_light6 = lightList[5] if len(lightList) > 5 else None
self.f3d_light7 = lightList[6] if len(lightList) > 6 else None
def combiner_uses(
f3dMat: "F3DMaterialProperty",
checkList,
checkCycle1=True,
checkCycle2=True,
checkColor=True,
checkAlpha=True,
swapTexelsCycle2=True,
):
is_two_cycle = f3dMat.rdp_settings.g_mdsft_cycletype == "G_CYC_2CYCLE"
for i in range(1, 3):
if i == 1 and not checkCycle1 or i == 2 and (not checkCycle2 or not is_two_cycle):
continue
combiner = getattr(f3dMat, f"combiner{i}")
for isAlpha in [False, True]:
if not isAlpha and not checkColor or isAlpha and not checkAlpha:
continue
for letter in ["A", "B", "C", "D"]:
value = getattr(combiner, letter + ("_alpha" if isAlpha else ""))
if i == 2 and swapTexelsCycle2 and value.startswith("TEXEL"):
value = "TEXEL" + chr(ord(value[5]) ^ 1) # Swap 0 and 1
if value in checkList:
return True
return False
def combiner_uses_tex0(f3d_mat: "F3DMaterialProperty"):
return combiner_uses(f3d_mat, ["TEXEL0", "TEXEL0_ALPHA"])
def combiner_uses_tex1(f3d_mat: "F3DMaterialProperty"):
return combiner_uses(f3d_mat, ["TEXEL1", "TEXEL1_ALPHA"])
def all_combiner_uses(f3d_mat: "F3DMaterialProperty") -> dict[str, bool]:
use_tex0 = combiner_uses_tex0(f3d_mat)
use_tex1 = combiner_uses_tex1(f3d_mat)
useDict = {
"Texture": use_tex0 or use_tex1,
"Texture 0": use_tex0,
"Texture 1": use_tex1,
"Primitive": combiner_uses(
f3d_mat,
["PRIMITIVE", "PRIMITIVE_ALPHA", "PRIM_LOD_FRAC"],
),
"Environment": combiner_uses(f3d_mat, ["ENVIRONMENT", "ENV_ALPHA"]),
"Shade": combiner_uses(f3d_mat, ["SHADE"], checkAlpha=False),
"Shade Alpha": combiner_uses(f3d_mat, ["SHADE"], checkColor=False)
or combiner_uses(f3d_mat, ["SHADE_ALPHA"], checkAlpha=False),
"Key": combiner_uses(f3d_mat, ["CENTER", "SCALE"]),
"LOD Fraction": combiner_uses(f3d_mat, ["LOD_FRACTION"]),
"Convert": combiner_uses(f3d_mat, ["K4", "K5"]),
}
return useDict
def ui_geo_mode(settings, dataHolder, layout, useDropdown):
f3d = get_F3D_GBI()
inputGroup = layout.column()
if useDropdown:
inputGroup.prop(
dataHolder,
"menu_geo",
text="Geometry Mode Settings",
icon="TRIA_DOWN" if dataHolder.menu_geo else "TRIA_RIGHT",
)
if not useDropdown or dataHolder.menu_geo:
disable_dependent = False # Don't disable dependent props in world defaults
def should_draw(*props):
return any(settings.has_prop_in_ucode(prop) for prop in props)
def is_on(*props):
return all(settings.is_geo_mode_on(prop) for prop in props)
def draw_mode(layout: UILayout, *props):
failed = False
for prop in props:
if settings.has_prop_in_ucode(prop):
layout.prop(settings, prop)
else:
failed = True
return not failed
def indentGroup(parent: UILayout, textOrProp: Union[str, "F3DMaterialProperty"], isText: bool) -> UILayout:
if not isText and not should_draw(textOrProp):
return parent.column(align=True)
c = parent.column(align=True)
if isText:
c.label(text=textOrProp)
enable = True
else:
c.prop(settings, textOrProp)
enable = getattr(settings, textOrProp)
c = c.split(factor=0.1)
c.label(text="")
c = c.column(align=True)
c.enabled = enable or not disable_dependent
return c
ccWarnings = shadeInCC = False
blendWarnings = shadeInBlender = zInBlender = False
if isinstance(dataHolder, F3DMaterialProperty):
ccWarnings = True
ccUse = all_combiner_uses(dataHolder)
shadeInCC = ccUse["Shade"] or ccUse["Shade Alpha"]
disable_dependent = True
if settings.set_rendermode:
blendWarnings = True
shadeInBlender = settings.does_blender_use_input("G_BL_A_SHADE")
zInBlender = settings.z_cmp or settings.z_upd
draw_mode(inputGroup, "g_shade_smooth")
c = indentGroup(inputGroup, "g_lighting", False)
if ccWarnings and not shadeInCC and is_on("g_lighting") and not is_on("g_tex_gen"):
multilineLabel(c, "Shade not used in CC, can disable\nlighting.", icon="INFO")
draw_mode(c, "g_packed_normals", "g_lighting_specular", "g_ambocclusion", "g_fresnel_color")
if should_draw("g_tex_gen_linear"):
d = indentGroup(c, "g_tex_gen", False)
else:
draw_mode(c, "g_tex_gen")
d = c
draw_mode(d, "g_tex_gen_linear")
if is_ucode_t3d(f3d.F3D_VER):
shadeColorLabel = "Lighting * vertex color"
if is_on("g_fresnel_color"):
shadeColorLabel = "Fresnel"
elif not is_on("g_lighting") or is_on("g_lighttoalpha"):
shadeColorLabel = "Vertex color"
elif is_on("g_lighting") and is_on("g_packed_normals") and not is_on("g_lighttoalpha"):
shadeColorLabel = "Lighting * vertex color"
else:
shadeColorLabel = "Lighting"
inputGroup.column().label(text=f"Shade color = {shadeColorLabel}")
draw_mode(inputGroup, "g_fog")
shadowMapInShadeAlpha = False
if is_on("g_fog"):
shadeAlphaLabel = "Fog"
elif is_on("g_lighting", "g_fresnel_alpha"):
shadeAlphaLabel = "Fresnel"
elif is_on("g_lighting", "g_lighttoalpha"):
shadeAlphaLabel = "Light intensity"
elif is_on("g_lighting", "g_ambocclusion"):
shadeAlphaLabel = "Shadow map / AO in vtx alpha"
shadowMapInShadeAlpha = True
else:
shadeAlphaLabel = "Vtx alpha"
warnings, errors = [], []
if is_on("g_lighting", "g_fog", "g_fresnel_alpha"):
errors.append("Fog overrides Fresnel Alpha.")
if is_on("g_lighting", "g_fog", "g_lighttoalpha"):
errors.append("Fog overrides Light-to-Alpha.")
if is_on("g_lighting", "g_fresnel_alpha", "g_lighttoalpha"):
errors.append("Fresnel Alpha overrides Light-to-Alpha.")
if shadowMapInShadeAlpha and ccWarnings and ccUse["Shade Alpha"]:
warnings.append("Shadow map = shade alpha used in CC, probably wrong.")
if is_on("g_fog") and ccWarnings and ccUse["Shade Alpha"]:
warnings.append("Fog = shade alpha used in CC, probably wrong.")
if blendWarnings and shadeInBlender and not is_on("g_fog"):
warnings.append("Rendermode uses shade alpha, probably fog.")
elif blendWarnings and not shadeInBlender and is_on("g_fog"):
warnings.append("Fog not used in rendermode / blender, can disable.")
if should_draw("g_lighttoalpha", "g_fresnel_alpha") or warnings or errors:
c = indentGroup(inputGroup, f"Shade alpha = {shadeAlphaLabel}:", True)
draw_mode(c, "g_lighttoalpha", "g_fresnel_alpha")
if warnings:
multilineLabel(c, "\n".join(warnings), "INFO")
if errors:
multilineLabel(c, "\n".join(errors), "ERROR")
else:
inputGroup.column().label(text=f"Shade alpha = {shadeAlphaLabel}")
if should_draw("g_attroffset_st_enable", "g_attroffset_z_enable"):
indentGroup(inputGroup, "Attribute offsets:", True)
draw_mode(inputGroup, "g_attroffset_st_enable", "g_attroffset_z_enable")
if should_draw("g_cull_front", "g_cull_back"):
c = indentGroup(inputGroup, "Face culling:", True)
draw_mode(c, "g_cull_front", "g_cull_back")
if is_on("g_cull_front", "g_cull_back"):
c.label(text="Nothing will be drawn.", icon="ERROR")
if should_draw("g_zbuffer", "g_shade"):
c = indentGroup(inputGroup, "Disable if not using:", True)
draw_mode(c, "g_zbuffer", "g_shade")
if blendWarnings and not is_on("g_zbuffer") and zInBlender:
c.label(text="Rendermode / blender using Z, must enable.", icon="ERROR")
elif blendWarnings and is_on("g_zbuffer") and not zInBlender:
c.label(text="Z is not being used, can disable.", icon="INFO")
if ccWarnings and not is_on("g_shade") and (shadeInCC or shadeInBlender):
if shadeInCC and shadeInBlender:
where = "CC and blender"
elif shadeInCC:
where = "CC"
else:
where = "rendermode / blender"
c.label(text=f"Shade in use in {where}, must enable.", icon="ERROR")
elif ccWarnings and is_on("g_shade") and not shadeInCC and not shadeInBlender:
c.label(text="Shade is not being used, can disable.", icon="INFO")
if should_draw("g_lod", "g_clipping"):
c = indentGroup(inputGroup, "Not useful:", True)
draw_mode(c, "g_lod", "g_clipping")
def ui_upper_mode(settings, dataHolder, layout: UILayout, useDropdown):
inputGroup: UILayout = layout.column()
if useDropdown:
inputGroup.prop(
dataHolder,
"menu_upper",
text="Other Mode Upper Settings",
icon="TRIA_DOWN" if dataHolder.menu_upper else "TRIA_RIGHT",
)
if not useDropdown or dataHolder.menu_upper:
prop_split(inputGroup, settings, "g_mdsft_alpha_dither", "Alpha Dither")
prop_split(inputGroup, settings, "g_mdsft_rgb_dither", "RGB Dither")
prop_split(inputGroup, settings, "g_mdsft_combkey", "Chroma Key")
prop_split(inputGroup, settings, "g_mdsft_textconv", "Texture Convert")
prop_split(inputGroup, settings, "g_mdsft_text_filt", "Texture Filter")
textlut_col = inputGroup.column()
tlut_mode = get_textlut_mode(dataHolder, True) if isinstance(dataHolder, F3DMaterialProperty) else None
if tlut_mode:
textlut_col.enabled = False
split = textlut_col.split(factor=0.5)
split.label(text="Texture LUT (Auto)")
box = split.box()
box.label(text={"G_TT_NONE": "None"}.get(tlut_mode, tlut_mode.lstrip("G_TT_")))
box.scale_y = 0.5
else:
prop_split(textlut_col, settings, "g_mdsft_textlut", "Texture LUT")
prop_split(inputGroup, settings, "g_mdsft_textlod", "Texture LOD (Mipmapping)")
if settings.g_mdsft_textlod == "G_TL_LOD":
inputGroup.prop(settings, "num_textures_mipmapped", text="Number of Mipmaps")
if settings.num_textures_mipmapped > 2:
box = inputGroup.box()
box.alert = True
box.label(
text="WARNING: Fast64 does not support setting more than two textures.", icon="LIBRARY_DATA_BROKEN"
)
box.label(text="Additional texture tiles will need to be set up manually.")
prop_split(inputGroup, settings, "g_mdsft_textdetail", "Texture Detail")
prop_split(inputGroup, settings, "g_mdsft_textpersp", "Texture Perspective Correction")
prop_split(inputGroup, settings, "g_mdsft_cycletype", "Cycle Type")
prop_split(inputGroup, settings, "g_mdsft_pipeline", "Pipeline Span Buffer Coherency")
def ui_lower_mode(settings, dataHolder, layout: UILayout, useDropdown):
inputGroup: UILayout = layout.column()
if useDropdown:
inputGroup.prop(
dataHolder,
"menu_lower",
text="Other Mode Lower Settings",
icon="TRIA_DOWN" if dataHolder.menu_lower else "TRIA_RIGHT",
)
if not useDropdown or dataHolder.menu_lower:
prop_split(inputGroup, settings, "g_mdsft_alpha_compare", "Alpha Compare")
prop_split(inputGroup, settings, "g_mdsft_zsrcsel", "Z Source Selection")
if settings.g_mdsft_zsrcsel == "G_ZS_PRIM":
prim_box = inputGroup.box()
prop_split(prim_box, settings.prim_depth, "z", "Prim Depth: Z")
prop_split(prim_box, settings.prim_depth, "dz", "Prim Depth: Delta Z")
if settings.prim_depth.dz != 0 and settings.prim_depth.dz & (settings.prim_depth.dz - 1):
prim_box.label(text="Warning: DZ should ideally be a power of 2 up to 0x4000", icon="TEXTURE_DATA")
def ui_other(settings, dataHolder, layout, useDropdown):
inputGroup = layout.column()
if useDropdown:
inputGroup.prop(
dataHolder, "menu_other", text="Other Settings", icon="TRIA_DOWN" if dataHolder.menu_other else "TRIA_RIGHT"
)
if not useDropdown or dataHolder.menu_other:
if "Clip Ratio" in sources_in_ucode(bpy.context.scene.f3d_type):
clipRatioGroup = inputGroup.column()
prop_split(clipRatioGroup, settings, "clip_ratio", "Clip Ratio")
if isinstance(dataHolder, Material) or isinstance(dataHolder, F3DMaterialProperty):
blend_color_group = layout.row()
prop_input_name = blend_color_group.column()
prop_input = blend_color_group.column()
prop_input_name.prop(dataHolder, "set_blend", text="Blend Color")
prop_input.prop(dataHolder, "blend_color", text="")
prop_input.enabled = dataHolder.set_blend
def tmemUsageUI(layout, textureProp):
tex = textureProp.tex
if tex is not None and tex.size[0] > 0 and tex.size[1] > 0:
tmemUsage = getTmemWordUsage(textureProp.tex_format, tex.size[0], tex.size[1]) * 8
tmemMax = getTmemMax(textureProp.tex_format)
layout.label(text="TMEM Usage: " + str(tmemUsage) + " / " + str(tmemMax) + " bytes")
if tmemUsage > tmemMax:
tmemSizeWarning = layout.box()
tmemSizeWarning.label(text="WARNING: Texture size is too large.")
tmemSizeWarning.label(text="Note that width will be internally padded to 64 bit boundaries.")
# UI Assumptions:
# shading = 1
# lighting = 1
# cycle type = 1 cycle
class F3DPanel(Panel):
bl_label = "F3D Material"
bl_idname = "MATERIAL_PT_F3D_Inspector"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_options = {"HIDE_HEADER"}
def ui_prop(self, material, layout, name, setName, setProp, showCheckBox):
nodes = material.node_tree.nodes
inputGroup = layout.row()
prop_input_name = inputGroup.column()
prop_input = inputGroup.column()
if showCheckBox:
prop_input_name.prop(material, setName, text=name)
else:
prop_input_name.label(text=name)
prop_input.prop(nodes[name].outputs[0], "default_value", text="")
prop_input.enabled = setProp
return inputGroup
def ui_prop_non_node(self, material, layout, label, name, setName, setProp):
inputGroup = layout.row()
prop_input_name = inputGroup.column()
prop_input = inputGroup.column()
prop_input_name.prop(material, setName, text=name)
prop_input.prop(material, name, text="")
prop_input.enabled = setProp
return inputGroup
def ui_large(self, material, layout):
layout.prop(material, "use_large_textures")
if material.use_large_textures:
inputGroup = layout.row().split(factor=0.5)
inputGroup.label(text="Large texture edges:")
inputGroup.prop(material, "large_edges", text="")
def ui_scale(self, material, layout):
inputGroup = layout.row().split(factor=0.5)
prop_input = inputGroup.column()
prop_input.prop(material, "scale_autoprop", text="Texture Auto Scale")
prop_input_group = inputGroup.row()
prop_input_group.prop(material, "tex_scale", text="")
prop_input_group.enabled = not material.scale_autoprop
return inputGroup
def ui_prim(self, material, layout, setName, setProp, showCheckBox):
f3dMat = material.f3d_mat
inputGroup = layout.row()
prop_input_name = inputGroup.column()
prop_input = inputGroup.column()
if showCheckBox:
prop_input_name.prop(f3dMat, setName, text="Primitive Color")
else:
prop_input_name.label(text="Primitive Color")
prop_input.prop(f3dMat, "prim_color", text="")
prop_input.prop(f3dMat, "prim_lod_frac", text="Prim LOD Fraction")
prop_input.prop(f3dMat, "prim_lod_min", text="Min LOD Ratio")
prop_input.enabled = setProp
return inputGroup
def ui_env(self, material, layout, showCheckBox):
inputGroup = layout.row()
prop_input_name = inputGroup.column()
prop_input = inputGroup.column()
if showCheckBox:
prop_input_name.prop(material.f3d_mat, "set_env", text="Environment Color")
else:
prop_input_name.label(text="Environment Color")
prop_input.prop(material.f3d_mat, "env_color", text="")
setProp = material.f3d_mat.set_env
prop_input.enabled = setProp
return inputGroup
def ui_chroma(self, material, layout, name, setName, setProp, showCheckBox):
inputGroup = layout.row()
prop_input_name = inputGroup.column()
prop_input = inputGroup.column()
f3d_mat = material.f3d_mat
if showCheckBox:
prop_input_name.prop(f3d_mat, setName, text="Chroma Key")
else:
prop_input_name.label(text="Chroma Key")
prop_input.prop(f3d_mat, "key_center", text="Center")
prop_input.prop(f3d_mat, "key_scale", text="Scale")
prop_input.prop(f3d_mat, "key_width", text="Width")
if f3d_mat.key_width[0] > 1 or f3d_mat.key_width[1] > 1 or f3d_mat.key_width[2] > 1:
layout.box().label(text="NOTE: Keying is disabled for channels with width > 1.")
prop_input.enabled = setProp
return inputGroup
def ui_lights(self, f3d_mat: "F3DMaterialProperty", layout: UILayout, name, showCheckBox):
if inherit_light_and_fog():
return
inputGroup = layout.row()
prop_input_left = inputGroup.column()
prop_input = inputGroup.column()
if showCheckBox:
prop_input_left.prop(f3d_mat, "set_lights", text=name)
else:
prop_input_left.label(text=name)
settings = f3d_mat.rdp_settings
prop_input_left.enabled = settings.is_geo_mode_on("g_lighting") and settings.is_geo_mode_on("g_shade")
lightSettings: UILayout = prop_input.column()
if settings.is_geo_mode_on("g_lighting"):
prop_input_left.separator(factor=0.25)
light_controls = prop_input_left.box()
light_controls.enabled = f3d_mat.set_lights
light_controls.prop(f3d_mat, "use_default_lighting", text="Use Custom Lighting", invert_checkbox=True)
if f3d_mat.use_default_lighting:
lightSettings.prop(f3d_mat, "default_light_color", text="Light Color")
light_controls.prop(f3d_mat, "set_ambient_from_light", text="Automatic Ambient Color")
ambCol = lightSettings.column()
ambCol.enabled = not f3d_mat.set_ambient_from_light
ambCol.prop(f3d_mat, "ambient_light_color", text="Ambient Color")
else:
lightSettings.prop(f3d_mat, "ambient_light_color", text="Ambient Color")
lightSettings.prop_search(f3d_mat, "f3d_light1", bpy.data, "lights", text="")
if f3d_mat.f3d_light1 is not None:
lightSettings.prop_search(f3d_mat, "f3d_light2", bpy.data, "lights", text="")
if f3d_mat.f3d_light2 is not None:
lightSettings.prop_search(f3d_mat, "f3d_light3", bpy.data, "lights", text="")
if f3d_mat.f3d_light3 is not None:
lightSettings.prop_search(f3d_mat, "f3d_light4", bpy.data, "lights", text="")
if f3d_mat.f3d_light4 is not None:
lightSettings.prop_search(f3d_mat, "f3d_light5", bpy.data, "lights", text="")
if f3d_mat.f3d_light5 is not None:
lightSettings.prop_search(f3d_mat, "f3d_light6", bpy.data, "lights", text="")
if f3d_mat.f3d_light6 is not None:
lightSettings.prop_search(f3d_mat, "f3d_light7", bpy.data, "lights", text="")
return inputGroup
def ui_convert(self, material, layout, showCheckBox):
inputGroup = layout.row()
prop_input_name = inputGroup.column()
prop_input = inputGroup.column()
if showCheckBox:
prop_input_name.prop(material, "set_k0_5", text="YUV Convert")
else:
prop_input_name.label(text="YUV Convert")
prop_k0 = prop_input.row()
prop_k0.prop(material, "k0", text="K0")
prop_k0.label(text=str(int(material.k0 * 255)))
prop_k1 = prop_input.row()
prop_k1.prop(material, "k1", text="K1")
prop_k1.label(text=str(int(material.k1 * 255)))
prop_k2 = prop_input.row()
prop_k2.prop(material, "k2", text="K2")
prop_k2.label(text=str(int(material.k2 * 255)))
prop_k3 = prop_input.row()
prop_k3.prop(material, "k3", text="K3")
prop_k3.label(text=str(int(material.k3 * 255)))
prop_k4 = prop_input.row()
prop_k4.prop(material, "k4", text="K4")
prop_k4.label(text=str(int(material.k4 * 255)))
prop_k5 = prop_input.row()
prop_k5.prop(material, "k5", text="K5")
prop_k5.label(text=str(int(material.k5 * 255)))
prop_input.enabled = material.set_k0_5
return inputGroup
def ui_lower_render_mode(self, material, layout, useDropdown):
is_two_cycle = material.rdp_settings.g_mdsft_cycletype == "G_CYC_2CYCLE"
# cycle independent
inputGroup = layout.column()
if useDropdown:
inputGroup.prop(
material,
"menu_lower_render",
text="Render Settings",
icon="TRIA_DOWN" if material.menu_lower_render else "TRIA_RIGHT",
)
if not useDropdown or material.menu_lower_render:
inputGroup.prop(material.rdp_settings, "set_rendermode", text="Set Render Mode?")
renderGroup = inputGroup.column()
renderGroup.prop(material.rdp_settings, "rendermode_advanced_enabled", text="Show Advanced Settings")
if not material.rdp_settings.rendermode_advanced_enabled:
f3d = get_F3D_GBI()
prop_split(renderGroup, material.rdp_settings, "rendermode_preset_cycle_1", "Render Mode")
no_flags_1 = material.rdp_settings.rendermode_preset_cycle_1 in f3d.rendermodePresetsWithoutFlags
if is_two_cycle:
prop_split(renderGroup, material.rdp_settings, "rendermode_preset_cycle_2", "Render Mode Cycle 2")
no_flags_2 = material.rdp_settings.rendermode_preset_cycle_2 in f3d.rendermodePresetsWithoutFlags
if no_flags_1 and no_flags_2:
multilineLabel(
renderGroup.box(),
"Invalid combination of rendermode presets.\n"
+ "Neither of these presets sets the rendermode flags.",
"ERROR",
)
elif not no_flags_1 and not no_flags_2:
multilineLabel(
renderGroup.box(),
"Invalid combination of rendermode presets.\n"
+ "Both of these presets set the rendermode flags.",
"ERROR",
)
else:
if no_flags_1:
multilineLabel(
renderGroup.box(),
"Invalid rendermode preset in 1-cycle.\n"
+ "This preset does not set the rendermode flags.",
"ERROR",
)
else:
prop_split(renderGroup, material.rdp_settings, "aa_en", "Antialiasing")
prop_split(renderGroup, material.rdp_settings, "z_cmp", "Z Testing")
prop_split(renderGroup, material.rdp_settings, "z_upd", "Z Writing")
prop_split(renderGroup, material.rdp_settings, "im_rd", "IM_RD (?)")
prop_split(renderGroup, material.rdp_settings, "clr_on_cvg", "Color On Coverage")
prop_split(renderGroup, material.rdp_settings, "cvg_dst", "Coverage Destination")
prop_split(renderGroup, material.rdp_settings, "zmode", "Z Mode")
prop_split(renderGroup, material.rdp_settings, "cvg_x_alpha", "Multiply Coverage And Alpha")
prop_split(renderGroup, material.rdp_settings, "alpha_cvg_sel", "Use Coverage For Alpha")
prop_split(renderGroup, material.rdp_settings, "force_bl", "Force Blending")
# cycle dependent - (P * A + M - B) / (A + B)
combinerBox = renderGroup.box()
combinerBox.label(text="Blender (Color = (P * A + M * B) / (A + B)")
combinerCol = combinerBox.row()
rowColor = combinerCol.column()
rowAlpha = combinerCol.column()
rowColor.prop(material.rdp_settings, "blend_p1", text="P")
rowColor.prop(material.rdp_settings, "blend_m1", text="M")
rowAlpha.prop(material.rdp_settings, "blend_a1", text="A")
rowAlpha.prop(material.rdp_settings, "blend_b1", text="B")
if is_two_cycle:
combinerBox2 = renderGroup.box()
combinerBox2.label(text="Blender Cycle 2")
combinerCol2 = combinerBox2.row()
rowColor2 = combinerCol2.column()
rowAlpha2 = combinerCol2.column()
rowColor2.prop(material.rdp_settings, "blend_p2", text="P")
rowColor2.prop(material.rdp_settings, "blend_m2", text="M")
rowAlpha2.prop(material.rdp_settings, "blend_a2", text="A")
rowAlpha2.prop(material.rdp_settings, "blend_b2", text="B")
if is_two_cycle:
if (
material.rdp_settings.blend_b1 == "G_BL_A_MEM"
or material.rdp_settings.blend_p1 == "G_BL_CLR_MEM"
or material.rdp_settings.blend_m1 == "G_BL_CLR_MEM"
):
multilineLabel(
renderGroup.box(),
"RDP silicon bug: Framebuffer color / alpha in blender\n"
+ "cycle 1 is broken, actually value from PREVIOUS pixel.",
"ORPHAN_DATA",
)
if material.rdp_settings.blend_a2 == "G_BL_A_SHADE":
multilineLabel(
renderGroup.box(),
"RDP silicon bug: Shade alpha in blender cycle 2\n"
+ "is broken, actually shade alpha from NEXT pixel.",
"ORPHAN_DATA",
)
renderGroup.enabled = material.rdp_settings.set_rendermode
def ui_uvCheck(self, layout, context):
if hasattr(context, "object") and context.object is not None and isinstance(context.object.data, Mesh):
uv_layers = context.object.data.uv_layers
if uv_layers.active is None or uv_layers.active.name != "UVMap":
uvErrorBox = layout.box()
uvErrorBox.label(text='Warning: This mesh\'s active UV layer is not named "UVMap".')
uvErrorBox.label(text="This will cause incorrect UVs to display.")
def ui_draw_layer(self, material, layout, context):
if context.scene.gameEditorMode == "SM64":
prop_split(layout, material.f3d_mat.draw_layer, "sm64", "Draw Layer")
elif context.scene.gameEditorMode in {"OOT", "MM"}:
prop_split(layout, material.f3d_mat.draw_layer, "oot", "Draw Layer")
def ui_misc(self, f3dMat: "F3DMaterialProperty", inputCol: UILayout, showCheckBox: bool) -> None:
sources = sources_in_ucode(bpy.context.scene.f3d_type)
if "AO" in sources and f3dMat.rdp_settings.g_ambocclusion:
if showCheckBox or f3dMat.set_ao:
inputGroup = inputCol.column()
if showCheckBox:
inputGroup.prop(f3dMat, "set_ao", text="Set Ambient Occlusion")
if f3dMat.set_ao:
prop_split(inputGroup.row(), f3dMat, "ao_ambient", "AO Ambient")
prop_split(inputGroup.row(), f3dMat, "ao_directional", "AO Directional")
prop_split(inputGroup.row(), f3dMat, "ao_point", "AO Point")
if "Fresnel" in sources and f3dMat.rdp_settings.g_fresnel_color or f3dMat.rdp_settings.g_fresnel_alpha:
if showCheckBox or f3dMat.set_fresnel:
inputGroup = inputCol.column()
if showCheckBox:
inputGroup.prop(f3dMat, "set_fresnel", text="Set Fresnel")
if f3dMat.set_fresnel:
prop_split(inputGroup.row(), f3dMat, "fresnel_lo", "Fresnel Lo")
prop_split(inputGroup.row(), f3dMat, "fresnel_hi", "Fresnel Hi")
if "ST Attr Offset" in sources and f3dMat.rdp_settings.g_attroffset_st_enable:
if showCheckBox or f3dMat.set_attroffs_st:
inputGroup = inputCol.column()
if showCheckBox:
inputGroup.prop(f3dMat, "set_attroffs_st", text="Set ST Attr Offset")
if f3dMat.set_attroffs_st:
prop_split(inputGroup.row(), f3dMat, "attroffs_st", "ST Attr Offset")
if "Z Attr Offset" in sources and f3dMat.rdp_settings.g_attroffset_z_enable:
if showCheckBox or f3dMat.set_attroffs_z:
inputGroup = inputCol.column()
if showCheckBox:
inputGroup.prop(f3dMat, "set_attroffs_z", text="Set Z Attr Offset")
if f3dMat.set_attroffs_z:
prop_split(inputGroup.row(), f3dMat, "attroffs_z", "Z Attr Offset")
if "Fog" in sources and f3dMat.rdp_settings.using_fog and not inherit_light_and_fog():
if showCheckBox or f3dMat.set_fog:
inputGroup = inputCol.column()
if showCheckBox:
inputGroup.prop(f3dMat, "set_fog", text="Set Fog")
if f3dMat.set_fog:
draw_fog = True
if bpy.context.scene.gameEditorMode == "SM64":
obj, area_obj = bpy.context.object.parent, None
while obj:
if obj.type == "EMPTY" and obj.sm64_obj_type == "Area Root" and obj.fast64.sm64.area.set_fog:
area_obj = obj
break
obj = obj.parent
if area_obj:
inputGroup.prop(f3dMat, "use_global_fog", text=f'Use Area "{area_obj.name}"\'s Fog')
if f3dMat.use_global_fog:
settings_col = inputGroup.column()
settings_col.enabled = not f3dMat.use_global_fog
prop_split(settings_col.row(), area_obj, "area_fog_color", "Fog Color")
prop_split(settings_col.row(), area_obj, "area_fog_position", "Fog Range")
draw_fog = False
else:
# show setting for preview
inputGroup.prop(f3dMat, "use_global_fog", text="Use Area's Fog")
inputGroup.label(
text="Preview only in this context, no area fog settings to pick up", icon="INFO"
)
if draw_fog:
prop_split(inputGroup.row(), f3dMat, "fog_color", "Fog Color")
prop_split(inputGroup.row(), f3dMat, "fog_position", "Fog Range")
def ui_cel_shading(self, material: Material, layout: UILayout):
inputGroup = layout.box().column()
r = inputGroup.row(align=True)
r.prop(
material.f3d_mat,
"expand_cel_shading_ui",
text="",
icon="TRIA_DOWN" if material.f3d_mat.expand_cel_shading_ui else "TRIA_RIGHT",
icon_only=True,
emboss=False,
)
r.prop(material.f3d_mat, "use_cel_shading")
if not material.f3d_mat.expand_cel_shading_ui:
return
if not material.f3d_mat.use_cel_shading:
inputGroup = inputGroup.column()
inputGroup.enabled = False
cel = material.f3d_mat.cel_shading
prop_split(inputGroup.row(), cel, "tintPipeline", "Tint pipeline:")
prop_split(inputGroup.row(), cel, "cutoutSource", "Cutout:")
if material.f3d_mat.rdp_settings.zmode != "ZMODE_OPA":
inputGroup.label(text="zmode in blender / rendermode must be opaque.", icon="ERROR")
if cel.cutoutSource == "ENVIRONMENT":
if not material.f3d_mat.set_env or material.f3d_mat.env_color[3] != 1.0:
inputGroup.label(text="Enable env color, and set env alpha to 255.", icon="ERROR")
else:
tex = material.f3d_mat.tex0 if cel.cutoutSource == "TEXEL0" else material.f3d_mat.tex1
if tex.tex is None or not tex.tex_set:
inputGroup.label(text=f"Texture {cel.cutoutSource[5]} is not set up correctly.", icon="ERROR")
if (
len(cel.levels) >= 3
and cel.levels[0].threshMode == cel.levels[1].threshMode
and not all([cel.levels[0].threshMode == lvl.threshMode for lvl in cel.levels[1:]])
):
multilineLabel(
inputGroup.box(),
"If using both lighter and darker cel\n" + "levels, one of each must be at the beginning",
"ERROR",
)
r = inputGroup.row(align=True)
r.label(text="Cel levels:")
op = r.operator(CelLevelAdd.bl_idname, text="", icon="ADD")
op.materialName = material.name
if len(cel.levels) > 0:
op = r.operator(CelLevelRemove.bl_idname, text="", icon="REMOVE")
op.materialName = material.name
showSegHelp = False
for level in cel.levels:
box = inputGroup.box().column()
r = box.row().split(factor=0.2)
r.label(text="Draw when")
r = r.split(factor=0.3)
r.prop(level, "threshMode", text="")
r = r.split(factor=0.2)
r.label(text="than")
r.prop(level, "threshold")
r = box.row().split(factor=0.08)
r.label(text="Tint:")
r = r.split(factor=0.27)
r.prop(level, "tintType", text="")
r = r.split(factor=0.45)
if level.tintType == "Fixed":
r.prop(level, "tintFixedLevel")
r = r.split(factor=0.3)
r.label(text="Color:")
r.prop(level, "tintFixedColor", text="")
elif level.tintType == "Segment":
r.prop(level, "tintSegmentNum")
r.prop(level, "tintSegmentOffset")
showSegHelp = True
elif level.tintType == "Light":
r.prop(level, "tintFixedLevel")
r.prop(level, "tintLightSlot")
else:
raise PluginError("Invalid tintType")
if showSegHelp:
tintName, tintNameCap = ("prim", "Prim") if cel.tintPipeline == "CC" else ("fog", "Fog")
multilineLabel(
inputGroup,
"Segments: In your code, set up DL in segment(s) used with\n"
+ f"gsDPSet{tintNameCap}Color then gsSPEndDisplayList at appropriate offset\n"
+ f"with {tintName} color = tint color and {tintName} alpha = tint level.",
"INFO",
)
def checkDrawLayersWarnings(self, f3dMat: "F3DMaterialProperty", useDict: Dict[str, bool], layout: UILayout):
settings = f3dMat.rdp_settings
f3d_type = bpy.context.scene.f3d_type
anyUseShadeAlpha = useDict["Shade Alpha"] or settings.does_blender_use_input("G_BL_A_SHADE")
g_lighting = settings.is_geo_mode_on("g_lighting") or is_ucode_t3d(f3d_type)
g_fog = settings.is_geo_mode_on("g_fog")
g_packed_normals = settings.is_geo_mode_on("g_packed_normals") or is_ucode_t3d(f3d_type)
g_ambocclusion = settings.is_geo_mode_on("g_ambocclusion")
g_lighttoalpha = settings.is_geo_mode_on("g_lighttoalpha")
g_fresnel_color = settings.is_geo_mode_on("g_fresnel_color")
g_fresnel_alpha = settings.is_geo_mode_on("g_fresnel_alpha")
usesVertexColor = useDict["Shade"] and (not g_lighting or (g_packed_normals and not g_fresnel_color))
usesVertexAlpha = anyUseShadeAlpha and (g_ambocclusion or not (g_fog or g_lighttoalpha or g_fresnel_alpha))
if not usesVertexColor and not usesVertexAlpha:
return
noticeBox = layout.box().column()
if not usesVertexColor:
noticeBox.label(text='Mesh must have Color Attribute (vtx color) layer called "Alpha".', icon="IMAGE_ALPHA")
elif not usesVertexAlpha:
noticeBox.label(
text='Mesh must have Color Attribute (vtx color) layer called "Col".', icon="IMAGE_RGB_ALPHA"
)
else:
noticeBox.label(text="Mesh must have two Color Attribute (vtx color) layers.", icon="IMAGE_RGB_ALPHA")
noticeBox.label(text='They must be called "Col" and "Alpha".', icon="IMAGE_ALPHA")
def checkDrawMixedCIWarning(self, layout, useDict, f3dMat):
useTex0 = useDict["Texture 0"] and f3dMat.tex0.tex_set
useTex1 = useDict["Texture 1"] and f3dMat.tex1.tex_set
if not useTex0 or not useTex1:
return
isTex0CI = f3dMat.tex0.tex_format[:2] == "CI"
isTex1CI = f3dMat.tex1.tex_format[:2] == "CI"
if isTex0CI != isTex1CI:
layout.box().column().label(text="Can't have one CI tex and one non-CI.", icon="ERROR")
if isTex0CI and isTex1CI and (f3dMat.tex0.ci_format != f3dMat.tex1.ci_format):
layout.box().column().label(text="Two CI textures must use the same CI format.", icon="ERROR")
def draw_simple(self, f3dMat, material, layout, context):
f3d = get_F3D_GBI()
self.ui_uvCheck(layout, context)
inputCol = layout.column()
useDict = all_combiner_uses(f3dMat)
self.checkDrawLayersWarnings(f3dMat, useDict, layout)
useMultitexture = useDict["Texture 0"] and useDict["Texture 1"] and f3dMat.tex0.tex_set and f3dMat.tex1.tex_set
self.checkDrawMixedCIWarning(inputCol, useDict, f3dMat)
canUseLargeTextures = material.mat_ver > 3 and material.f3d_mat.use_large_textures
if useDict["Texture 0"] and f3dMat.tex0.tex_set:
ui_image(canUseLargeTextures, inputCol, material, f3dMat.tex0, "Texture 0", False)
if useDict["Texture 1"] and f3dMat.tex1.tex_set:
ui_image(canUseLargeTextures, inputCol, material, f3dMat.tex1, "Texture 1", False)
if useMultitexture:
inputCol.prop(f3dMat, "uv_basis", text="UV Basis")
if useDict["Texture"]:
self.ui_large(f3dMat, inputCol)
self.ui_scale(f3dMat, inputCol)
if useDict["Primitive"] and f3dMat.set_prim:
self.ui_prim(material, inputCol, "set_prim", f3dMat.set_prim, False)
if useDict["Environment"] and f3dMat.set_env:
self.ui_env(material, inputCol, False)
showLightProperty = (
f3dMat.set_lights
and "Lighting" in sources_in_ucode(bpy.context.scene.f3d_type)
and f3dMat.rdp_settings.is_geo_mode_on("g_lighting")
and f3dMat.rdp_settings.is_geo_mode_on("g_shade")
)
if useDict["Shade"] and showLightProperty:
self.ui_lights(f3dMat, inputCol, "Lighting", False)
if useDict["Key"] and f3dMat.set_key:
self.ui_chroma(material, inputCol, "Chroma Key Center", "set_key", f3dMat.set_key, False)
if useDict["Convert"] and f3dMat.set_k0_5:
self.ui_convert(f3dMat, inputCol, False)
self.ui_misc(f3dMat, inputCol, False)
def draw_full(self, f3dMat, material, layout: UILayout, context):
layout.row().prop(material, "menu_tab", expand=True)
menuTab = material.menu_tab
useDict = all_combiner_uses(f3dMat)
f3d = get_F3D_GBI()
if menuTab == "Combiner":
self.ui_draw_layer(material, layout, context)
self.checkDrawLayersWarnings(f3dMat, useDict, layout)
def drawCCProps(ui: UILayout, combiner: "CombinerProperty", isAlpha: bool, enabled: bool = True) -> None:
ui = ui.column()
ui.enabled = enabled
for letter in ["A", "B", "C", "D"]:
r = ui.row().split(factor=0.25 if isAlpha else 0.1)
r.label(text=f"{letter}{' Alpha' if isAlpha else ''}:")
r.prop(combiner, f"{letter}{'_alpha' if isAlpha else ''}", text="")
is_two_cycle = f3dMat.rdp_settings.g_mdsft_cycletype == "G_CYC_2CYCLE"
combinerBox = layout.box()
combinerBox.prop(f3dMat, "set_combiner", text="Color Combiner (Color = (A - B) * C + D)")
combinerCol = combinerBox.row().split(factor=0.45)
combinerCol.enabled = f3dMat.set_combiner
drawCCProps(combinerCol, f3dMat.combiner1, False)
drawCCProps(combinerCol, f3dMat.combiner1, True, not f3dMat.use_cel_shading)
if f3dMat.use_cel_shading:
r = combinerBox.column().label(
text=f"CC alpha{' cycle 1' if is_two_cycle else ''} is occupied by cel shading."
)
if is_two_cycle:
combinerBox2 = layout.box()
combinerBox2.label(text="Color Combiner Cycle 2")
combinerBox2.enabled = f3dMat.set_combiner
combinerCol2 = combinerBox2.row().split(factor=0.45)
drawCCProps(combinerCol2, f3dMat.combiner2, False)
drawCCProps(combinerCol2, f3dMat.combiner2, True)
if combiner_uses(f3dMat, ["TEXEL0", "TEXEL0_ALPHA"], checkCycle1=False, swapTexelsCycle2=False):
combinerBox2.label(text="'Texture 0' in Cycle 2 is actually Texture 1.", icon="INFO")
if combiner_uses(f3dMat, ["TEXEL1", "TEXEL1_ALPHA"], checkCycle1=False, swapTexelsCycle2=False):
multilineLabel(
combinerBox2,
"RDP silicon bug: 'Texture 1' in Cycle 2 is actually\n"
+ "Texture 0 for the NEXT pixel, causes visual issues.",
"ORPHAN_DATA",
)
if menuTab == "Sources":
self.ui_uvCheck(layout, context)
inputCol = layout.column()
useMultitexture = useDict["Texture 0"] and useDict["Texture 1"]
self.checkDrawMixedCIWarning(inputCol, useDict, f3dMat)
canUseLargeTextures = material.mat_ver > 3 and material.f3d_mat.use_large_textures
if useDict["Texture 0"]:
ui_image(canUseLargeTextures, inputCol, material, f3dMat.tex0, "Texture 0", True)
if useDict["Texture 1"]:
ui_image(canUseLargeTextures, inputCol, material, f3dMat.tex1, "Texture 1", True)
if useMultitexture:
inputCol.prop(f3dMat, "uv_basis", text="UV Basis")
if useDict["Texture"]:
self.ui_large(f3dMat, inputCol)
self.ui_scale(f3dMat, inputCol)
if useDict["Primitive"]:
self.ui_prim(material, inputCol, "set_prim", f3dMat.set_prim, True)
if useDict["Environment"]:
self.ui_env(material, inputCol, True)
if useDict["Shade"] and "Lighting" in sources_in_ucode(bpy.context.scene.f3d_type):
self.ui_lights(f3dMat, inputCol, "Lighting", True)
if useDict["Key"]:
self.ui_chroma(material, inputCol, "Chroma Key Center", "set_key", f3dMat.set_key, True)
if useDict["Convert"]:
self.ui_convert(f3dMat, inputCol, True)
self.ui_misc(f3dMat, inputCol, True)
if menuTab == "Geo":
ui_geo_mode(f3dMat.rdp_settings, f3dMat, layout, False)
if menuTab == "Upper":
ui_upper_mode(f3dMat.rdp_settings, f3dMat, layout, False)
if menuTab == "Lower":
ui_lower_mode(f3dMat.rdp_settings, f3dMat, layout, False)
self.ui_lower_render_mode(f3dMat, layout, False)
ui_other(f3dMat.rdp_settings, f3dMat, layout, False)
# texture convert/LUT controlled by texture settings
# add node support for geo mode settings
def draw(self, context):
layout = self.layout
layout.operator(CreateFast3DMaterial.bl_idname)
material = context.material
if material is None:
return
elif not material.use_nodes or not material.is_f3d:
layout.label(text="This is not a Fast3D material.")
return
f3dMat = material.f3d_mat
settings = f3dMat.rdp_settings
layout.prop(context.scene, "f3d_simple", text="Show Simplified UI")
layout = layout.box()
titleCol = layout.column()
titleCol.box().label(text="F3D Material Inspector")
if material.mat_ver < 5:
box = layout.box().column()
box.label(
text=f"Material version is outdated (V{material.mat_ver})",
icon="ORPHAN_DATA",
)
box.operator("object.convert_f3d_update")
return
presetCol = layout.column()
split = presetCol.split(factor=0.33)
split.label(text="Preset")
row = split.row(align=True)
row.menu(MATERIAL_MT_f3d_presets.__name__, text=f3dMat.presetName)
row.operator(AddPresetF3D.bl_idname, text="", icon="ADD")
row.operator(AddPresetF3D.bl_idname, text="", icon="REMOVE").remove_active = True
if settings.g_mdsft_alpha_compare == "G_AC_THRESHOLD" and settings.g_mdsft_cycletype == "G_CYC_2CYCLE":
multilineLabel(
layout.box(),
"RDP silicon bug: Alpha compare in 2-cycle mode is broken.\n"
+ "Compares to FIRST cycle CC alpha output from NEXT pixel.",
"ORPHAN_DATA",
)
if context.scene.f3d_simple and f3dMat.presetName != "Custom":
self.draw_simple(f3dMat, material, layout, context)
else:
presetCol.prop(context.scene, "f3dUserPresetsOnly")
self.draw_full(f3dMat, material, layout, context)
if context.scene.f3d_type == "F3DEX3":
self.ui_cel_shading(material, layout)
else:
r = layout.row()
r.enabled = False
r.label(text="Use Cel Shading (requires F3DEX3)", icon="TRIA_RIGHT")
layout.operator(RecreateF3DNodes.bl_idname)
class F3DMeshPanel(Panel):
bl_label = "F3D Mesh Inspector"
bl_idname = "F3D_PT_Mesh_Inspector"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
return context.object.type == "MESH"
def draw(self, context):
new_gbi = not get_F3D_GBI().F3D_OLD_GBI
col = self.layout.box().column()
col.box().label(text=self.bl_label, icon="MESH_DATA")
row = col.row()
row.enabled = new_gbi
row.prop(context.object, "use_f3d_culling")
if not new_gbi:
col.label(text="Only available in F3DEX and up", icon="INFO")
def ui_tileScroll(tex, name, layout):
row = layout.row()
row.label(text=name)
row.prop(tex.tile_scroll, "s", text="S:")
row.prop(tex.tile_scroll, "t", text="T:")
row.prop(tex.tile_scroll, "interval", text="Interval:")
def ui_procAnimVecEnum(material, procAnimVec, layout, name, vecType, useDropdown, useTex0, useTex1):
layout = layout.box()
box = layout.column()
if useDropdown:
layout.prop(procAnimVec, "menu", text=name, icon="TRIA_DOWN" if procAnimVec.menu else "TRIA_RIGHT")
else:
layout.box().label(text=name)
if not useDropdown or procAnimVec.menu:
box = layout.column()
combinedOption = None
xCombined = procAnimVec.x.animType == "Rotation"
if xCombined:
combinedOption = procAnimVec.x.animType
yCombined = procAnimVec.y.animType == "Rotation"
if yCombined:
combinedOption = procAnimVec.y.animType
if not yCombined:
ui_procAnimFieldEnum(procAnimVec.x, box, vecType[0], "UV" if xCombined else None)
if not xCombined:
ui_procAnimFieldEnum(procAnimVec.y, box, vecType[1], "UV" if yCombined else None)
if len(vecType) > 2:
ui_procAnimFieldEnum(procAnimVec.z, box, vecType[2])
if xCombined or yCombined:
box.row().prop(procAnimVec, "pivot")
box.row().prop(procAnimVec, "angularSpeed")
if combinedOption == "Rotation":
pass
if useTex0 or useTex1:
layout.box().label(text="SM64 SetTileSize Texture Scroll")
if useTex0:
ui_tileScroll(material.tex0, "Texture 0 Speed", layout)
if useTex1:
ui_tileScroll(material.tex1, "Texture 1 Speed", layout)
def ui_procAnimFieldEnum(procAnimField, layout, name, overrideName):
box = layout
box.prop(procAnimField, "animType", text=name if overrideName is None else overrideName)
if overrideName is None:
if procAnimField.animType == "Linear":
split0 = box.row().split(factor=1)
split0.prop(procAnimField, "speed")
elif procAnimField.animType == "Sine":
split1 = box.row().split(factor=0.3333)
split1.prop(procAnimField, "amplitude")
split1.prop(procAnimField, "frequency")
split1.prop(procAnimField, "offset")
elif procAnimField.animType == "Noise":
box.row().prop(procAnimField, "noiseAmplitude")
def ui_procAnimField(procAnimField, layout, name):
box = layout
box.prop(procAnimField, "animate", text=name)
if procAnimField.animate:
if name not in "XYZ":
split0 = box.row().split(factor=1)
split0.prop(procAnimField, "speed")
split1 = box.row().split(factor=0.5)
split1.prop(procAnimField, "amplitude")
split1.prop(procAnimField, "frequency")
layout.row().prop(procAnimField, "spaceFrequency")
split2 = box.row().split(factor=0.5)
split2.prop(procAnimField, "offset")
split2.prop(procAnimField, "noiseAmplitude")
def ui_procAnim(material, layout, useTex0, useTex1, title, useDropdown):
ui_procAnimVecEnum(material.f3d_mat, material.f3d_mat.UVanim0, layout, title, "UV", useDropdown, useTex0, useTex1)
def update_node_values(self, context, update_preset):
if hasattr(context.scene, "world") and self == create_or_get_world(context.scene).rdp_defaults:
pass
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
update_node_values_of_material(material, context)
if update_preset:
material.f3d_mat.presetName = "Custom"
def update_all_node_values(material, context):
update_node_values_without_preset(material, context)
update_tex_values_manual(material, context)
update_rendermode_preset(material, context)
def update_all_material_nodes(self, context):
for material in bpy.data.materials:
if material.is_f3d and material.mat_ver >= F3D_MAT_CUR_VERSION:
with context.temp_override(material=material):
update_all_node_values(material, context)
def update_world_default_rendermode(self, context):
for material in bpy.data.materials:
if material.is_f3d and material.mat_ver >= F3D_MAT_CUR_VERSION:
with context.temp_override(material=material):
update_rendermode_preset(material, context)
def update_node_values_with_preset(self, context):
update_node_values(self, context, update_preset=True)
def update_node_values_without_preset(self, context):
update_node_values(self, context, update_preset=False)
def update_light_properties(self, context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
update_light_colors(material, context)
def update_cel_cutout_source(self, context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
if not material.f3d_mat.use_cel_shading:
return
f3dMat = material.f3d_mat
cel = f3dMat.cel_shading
firstDarker = len(cel.levels) >= 1 and cel.levels[0].threshMode == "Darker"
f3dMat.combiner1.A_alpha, f3dMat.combiner1.B_alpha = ("1", "SHADE") if firstDarker else ("SHADE", "0")
f3dMat.combiner1.C_alpha = cel.cutoutSource
f3dMat.combiner1.D_alpha = "0"
def update_rendermode_preset(self, context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if material:
rendermode_preset_to_advanced(material)
update_node_values_with_preset(self, context)
def getSocketFromCombinerToNodeDictColor(nodes, combinerInput):
nodeName, socketIndex = combinerToNodeDictColor[combinerInput]
return nodes[nodeName].outputs[socketIndex] if nodeName is not None else None
def getSocketFromCombinerToNodeDictAlpha(nodes, combinerInput):
nodeName, socketIndex = combinerToNodeDictAlpha[combinerInput]
return nodes[nodeName].outputs[socketIndex] if nodeName is not None else None
# Maps the color combiner input name to the corresponding node name and output socket name
color_combiner_inputs = {
"COMBINED": (None, "Color"),
"TEXEL0": ("Tex0_I", "Color"),
"TEXEL1": ("Tex1_I", "Color"),
"PRIMITIVE": ("CombinerInputs", "Prim Color"),
"SHADE": ("Shade Color", "Color"),
"ENVIRONMENT": ("CombinerInputs", "Env Color"),
"CENTER": ("CombinerInputs", "Chroma Key Center"),
"SCALE": ("CombinerInputs", "Chroma Key Scale"),
"COMBINED_ALPHA": (None, "Alpha"),
"TEXEL0_ALPHA": ("Tex0_I", "Alpha"),
"TEXEL1_ALPHA": ("Tex1_I", "Alpha"),
"PRIMITIVE_ALPHA": ("CombinerInputs", "Prim Alpha"),
"SHADE_ALPHA": ("Shade Color", "Alpha"),
"ENV_ALPHA": ("CombinerInputs", "Env Alpha"),
"LOD_FRACTION": ("CombinerInputs", "LOD Fraction"),
"PRIM_LOD_FRAC": ("CombinerInputs", "Prim LOD Fraction"),
"NOISE": ("CombinerInputs", "Noise"),
"K4": ("CombinerInputs", "YUVConvert K4"),
"K5": ("CombinerInputs", "YUVConvert K5"),
"1": ("CombinerInputs", "1"),
"0": (None, 0),
}
# Maps the alpha combiner input name to the corresponding node name and output name
alpha_combiner_inputs = {
"COMBINED": (None, "Alpha"),
"TEXEL0": ("Tex0_I", "Alpha"),
"TEXEL1": ("Tex1_I", "Alpha"),
"PRIMITIVE": ("CombinerInputs", "Prim Alpha"),
"SHADE": ("Shade Color", "Alpha"),
"ENVIRONMENT": ("CombinerInputs", "Env Alpha"),
"LOD_FRACTION": ("CombinerInputs", "LOD Fraction"),
"PRIM_LOD_FRAC": ("CombinerInputs", "Prim LOD Fraction"),
"1": ("CombinerInputs", "1"),
"0": (None, 0),
}
def remove_first_link_if_exists(material: Material, links: tuple[NodeLink]):
if len(links) > 0:
link = links[0]
material.node_tree.links.remove(link)
def link_if_none_exist(
material: Material, fromOutput: NodeSocket, toInput: NodeSocket
): # TODO: (V5) add output/input type annotations
if len(fromOutput.links) == 0:
material.node_tree.links.new(fromOutput, toInput)
swaps_tex01 = {
"TEXEL0": "TEXEL1",
"TEXEL0_ALPHA": "TEXEL1_ALPHA",
"TEXEL1": "TEXEL0",
"TEXEL1_ALPHA": "TEXEL0_ALPHA",
}
def update_node_combiner(material, combinerInputs, cycleIndex):
nodes = material.node_tree.nodes
if cycleIndex == 1:
cycle_node = nodes["Cycle_1"]
else:
cycle_node = nodes["Cycle_2"]
for i in range(8):
combiner_input = combinerInputs[i]
if cycleIndex == 2:
# Swap texel0 for texel1 and vise versa
combiner_input = swaps_tex01.get(combiner_input, combiner_input)
if combiner_input == "0":
for link in cycle_node.inputs[i].links:
material.node_tree.links.remove(link)
if i < 4:
node_name, output_key = color_combiner_inputs[combiner_input]
if cycleIndex == 2:
if combiner_input == "COMBINED":
node_name = "Combined_C"
output_key = 0 # using an index due to it being a reroute node
elif combiner_input == "COMBINED_ALPHA":
node_name = "Combined_A"
output_key = 0 # using an index due to it being a reroute node
if node_name is not None:
input_node = nodes[node_name]
input_value = input_node.outputs[output_key]
material.node_tree.links.new(cycle_node.inputs[i], input_value)
else:
node_name, output_key = alpha_combiner_inputs[combiner_input]
if cycleIndex == 2:
if combiner_input == "COMBINED":
node_name = "Combined_A"
output_key = 0 # using an index due to it being a reroute node
if node_name is not None:
input_node = nodes[node_name]
input_value = input_node.outputs[output_key]
material.node_tree.links.new(cycle_node.inputs[i], input_value)
def update_fog_nodes(material: Material, context: Context):
nodes = material.node_tree.nodes
f3dMat: "F3DMaterialProperty" = material.f3d_mat
fogBlender: ShaderNodeGroup = nodes["FogBlender"]
# if NOT setting rendermode, it is more likely that the user is setting
# rendermodes in code, so to be safe we'll enable fog. Plus we are checking
# that fog is enabled in the geometry mode, so if so that's probably the intent.
fogBlender.node_tree = bpy.data.node_groups[
("FogBlender_On" if is_blender_doing_fog(material.f3d_mat.rdp_settings) else "FogBlender_Off")
]
remove_first_link_if_exists(material, fogBlender.inputs["FogAmount"].links)
if material.f3d_mat.rdp_settings.g_fog:
material.node_tree.links.new(nodes["CalcFog"].outputs["FogAmount"], fogBlender.inputs["FogAmount"])
else: # If fog is not being calculated, pass in shade alpha
material.node_tree.links.new(nodes["Shade Color"].outputs["Alpha"], fogBlender.inputs["FogAmount"])
if (
(bpy.context.scene.gameEditorMode == "SM64" and f3dMat.use_global_fog)
or not f3dMat.set_fog
or inherit_light_and_fog()
): # Inherit fog
link_if_none_exist(material, nodes["SceneProperties"].outputs["FogColor"], nodes["FogColor"].inputs[0])
link_if_none_exist(material, nodes["GlobalFogColor"].outputs[0], fogBlender.inputs["Fog Color"])
link_if_none_exist(material, nodes["SceneProperties"].outputs["FogNear"], nodes["CalcFog"].inputs["FogNear"])
link_if_none_exist(material, nodes["SceneProperties"].outputs["FogFar"], nodes["CalcFog"].inputs["FogFar"])
else:
remove_first_link_if_exists(material, nodes["FogBlender"].inputs["Fog Color"].links)
remove_first_link_if_exists(material, nodes["CalcFog"].inputs["FogNear"].links)
remove_first_link_if_exists(material, nodes["CalcFog"].inputs["FogFar"].links)
fogBlender.inputs["Fog Color"].default_value = s_rgb_alpha_1_tuple(f3dMat.fog_color)
nodes["CalcFog"].inputs["FogNear"].default_value = f3dMat.fog_position[0]
nodes["CalcFog"].inputs["FogFar"].default_value = f3dMat.fog_position[1]
def update_noise_nodes(material: Material):
f3dMat: "F3DMaterialProperty" = material.f3d_mat
uses_noise = f3dMat.combiner1.A == "NOISE" or f3dMat.combiner2.A == "NOISE"
noise_group = bpy.data.node_groups["F3DNoise_Animated" if uses_noise else "F3DNoise_NonAnimated"]
nodes = material.node_tree.nodes
if nodes["F3DNoiseFactor"].node_tree is not noise_group:
nodes["F3DNoiseFactor"].node_tree = noise_group
def update_combiner_connections(material: Material, context: Context, combiner: int | None = None):
f3dMat: "F3DMaterialProperty" = material.f3d_mat
update_noise_nodes(material)
# Combiner can be specified for performance reasons
if not combiner or combiner == 1:
combinerInputs1 = [
f3dMat.combiner1.A,
f3dMat.combiner1.B,
f3dMat.combiner1.C,
f3dMat.combiner1.D,
f3dMat.combiner1.A_alpha,
f3dMat.combiner1.B_alpha,
f3dMat.combiner1.C_alpha,
f3dMat.combiner1.D_alpha,
]
update_node_combiner(material, combinerInputs1, 1)
if not combiner or combiner == 2:
combinerInputs2 = [
f3dMat.combiner2.A,
f3dMat.combiner2.B,
f3dMat.combiner2.C,
f3dMat.combiner2.D,
f3dMat.combiner2.A_alpha,
f3dMat.combiner2.B_alpha,
f3dMat.combiner2.C_alpha,
f3dMat.combiner2.D_alpha,
]
update_node_combiner(material, combinerInputs2, 2)
def set_output_node_groups(material: Material):
nodes = material.node_tree.nodes
output_node = nodes["OUTPUT"]
f3dMat: "F3DMaterialProperty" = material.f3d_mat
cycle = f3dMat.rdp_settings.g_mdsft_cycletype.lstrip("G_CYC_").rstrip("_CYCLE")
output_method = get_output_method(material)
if bpy.app.version < (4, 2, 0) and output_method == "CLIP":
output_method = "XLU"
material.alpha_threshold = 0.125
output_group_name = f"OUTPUT_{cycle}CYCLE_{output_method}"
output_group = bpy.data.node_groups[output_group_name]
output_node.node_tree = output_group
for inp in output_node.inputs:
remove_first_link_if_exists(material, inp.links)
output_node.inputs["Cycle_C_1"].default_value = (0.0, 0.0, 0.0, 1.0)
output_node.inputs["Cycle_A_1"].default_value = 0.5
output_node.inputs["Cycle_C_2"].default_value = (0.0, 0.0, 0.0, 1.0)
output_node.inputs["Cycle_A_2"].default_value = 0.5
if output_method == "CLIP":
output_node.inputs["Alpha Threshold"].default_value = 0.125
material.node_tree.links.new(nodes["Cycle_1"].outputs["Color"], output_node.inputs["Cycle_C_1"])
material.node_tree.links.new(nodes["Cycle_1"].outputs["Alpha"], output_node.inputs["Cycle_A_1"])
material.node_tree.links.new(nodes["FogBlender"].outputs["Color"], output_node.inputs["Cycle_C_2"])
material.node_tree.links.new(nodes["Cycle_2"].outputs["Alpha"], output_node.inputs["Cycle_A_2"])
material.node_tree.links.new(output_node.outputs[0], nodes["Material Output F3D"].inputs[0])
def update_light_colors(material, context):
f3dMat: "F3DMaterialProperty" = material.f3d_mat
nodes = material.node_tree.nodes
if f3dMat.use_default_lighting and f3dMat.set_ambient_from_light:
amb = Color(f3dMat.default_light_color[:3])
# dividing by 4.672 approximates to half of the light color's value after gamma correction is performed on both ambient and light colors
amb.v /= 4.672
new_amb = [c for c in amb]
new_amb.append(1.0)
f3dMat.ambient_light_color = new_amb
if f3dMat.set_lights or inherit_light_and_fog():
remove_first_link_if_exists(material, nodes["Shade Color"].inputs["AmbientColor"].links)
remove_first_link_if_exists(material, nodes["Shade Color"].inputs["Light0Color"].links)
remove_first_link_if_exists(material, nodes["Shade Color"].inputs["Light1Color"].links)
# TODO: feature to toggle gamma correction
light0 = f3dMat.default_light_color
light1 = [0.0, 0.0, 0.0, 1.0]
if not f3dMat.use_default_lighting:
light0 = f3dMat.f3d_light1.color if f3dMat.f3d_light1 is not None else [1.0, 1.0, 1.0, 1.0]
light1 = f3dMat.f3d_light2.color if f3dMat.f3d_light2 is not None else light1
nodes["Shade Color"].inputs["AmbientColor"].default_value = s_rgb_alpha_1_tuple(f3dMat.ambient_light_color)
nodes["Shade Color"].inputs["Light0Color"].default_value = s_rgb_alpha_1_tuple(light0)
nodes["Shade Color"].inputs["Light1Color"].default_value = s_rgb_alpha_1_tuple(light1)
else:
nodes["Shade Color"].inputs["AmbientColor"].default_value = (0.5, 0.5, 0.5, 1.0)
nodes["Shade Color"].inputs["Light0Color"].default_value = (1.0, 1.0, 1.0, 1.0)
nodes["Shade Color"].inputs["Light1Color"].default_value = (0.0, 0.0, 0.0, 1.0)
link_if_none_exist(material, nodes["AmbientColorOut"].outputs[0], nodes["Shade Color"].inputs["AmbientColor"])
link_if_none_exist(material, nodes["Light0ColorOut"].outputs[0], nodes["Shade Color"].inputs["Light0Color"])
link_if_none_exist(material, nodes["Light1ColorOut"].outputs[0], nodes["Shade Color"].inputs["Light1Color"])
def update_color_node(combiner_inputs, color: Color, prefix: str):
"""Function for updating either Prim or Env colors"""
# TODO: feature to toggle gamma correction
combiner_inputs[f"{prefix} Color"].default_value = s_rgb_alpha_1_tuple(color)
combiner_inputs[f"{prefix} Alpha"].default_value = color[3]
# prim_color | Prim
# env_color | Env
def get_color_input_update_callback(attr_name="", prefix=""):
def input_update_callback(self: Material, context: Context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
f3dMat: "F3DMaterialProperty" = material.f3d_mat
nodes = material.node_tree.nodes
combiner_inputs = nodes["CombinerInputs"].inputs
update_color_node(combiner_inputs, getattr(f3dMat, attr_name), prefix)
return input_update_callback
def update_node_values_of_material(material: Material, context):
check_or_ask_color_management(context)
update_blend_method(material, context)
if not has_f3d_nodes(material):
return
f3dMat: "F3DMaterialProperty" = material.f3d_mat
settings: RDPSettings = f3dMat.rdp_settings
update_combiner_connections(material, context)
set_output_node_groups(material)
nodes = material.node_tree.nodes
if (settings.is_geo_mode_on("g_lighting") or inherit_light_and_fog()) and settings.is_geo_mode_on("g_tex_gen"):
if settings.is_geo_mode_on("g_tex_gen_linear"):
nodes["UV"].node_tree = bpy.data.node_groups["UV_EnvMap_Linear"]
else:
nodes["UV"].node_tree = bpy.data.node_groups["UV_EnvMap"]
else:
nodes["UV"].node_tree = bpy.data.node_groups["UV"]
shdcol_inputs = nodes["Shade Color"].inputs
for propName in [
"g_ambocclusion",
"g_packed_normals",
"g_lighttoalpha",
"g_lighting_specular",
"g_fresnel_color",
"g_fresnel_alpha",
"g_fog",
"g_lighting",
]:
shdcol_inputs[propName.upper()].default_value = f3dMat.rdp_settings.is_geo_mode_on(propName)
if is_ucode_t3d(bpy.context.scene.f3d_type): # Tiny3d always uses lighting * vertex color
shdcol_inputs["G_LIGHTING"].default_value = shdcol_inputs["G_PACKED_NORMALS"].default_value = True
shdcol_inputs["AO Ambient"].default_value = f3dMat.ao_ambient
shdcol_inputs["AO Directional"].default_value = f3dMat.ao_directional
shdcol_inputs["AO Point"].default_value = f3dMat.ao_point
shdcol_inputs["Fresnel Lo"].default_value = f3dMat.fresnel_lo
shdcol_inputs["Fresnel Hi"].default_value = f3dMat.fresnel_hi
update_light_colors(material, context)
combiner_inputs = nodes["CombinerInputs"].inputs
update_color_node(combiner_inputs, f3dMat.prim_color, "Prim")
update_color_node(combiner_inputs, f3dMat.env_color, "Env")
combiner_inputs["Chroma Key Center"].default_value = (
f3dMat.key_center[0],
f3dMat.key_center[1],
f3dMat.key_center[2],
f3dMat.key_center[3],
)
combiner_inputs["Chroma Key Scale"].default_value = [value for value in f3dMat.key_scale] + [1]
combiner_inputs["Prim LOD Fraction"].default_value = f3dMat.prim_lod_frac
combiner_inputs["YUVConvert K4"].default_value = f3dMat.k4
combiner_inputs["YUVConvert K5"].default_value = f3dMat.k5
material.show_transparent_back = f3dMat.rdp_settings.g_cull_front
material.use_backface_culling = f3dMat.rdp_settings.g_cull_back
update_tex_values_manual(material, context)
update_fog_nodes(material, context)
def set_texture_settings_node(material: Material):
nodes = material.node_tree.nodes
textureSettings: ShaderNodeGroup = nodes["TextureSettings"]
desired_group = bpy.data.node_groups["TextureSettings_Lite"]
if (material.f3d_mat.tex0.tex and not material.f3d_mat.tex0.autoprop) or (
material.f3d_mat.tex1.tex and not material.f3d_mat.tex1.autoprop
):
desired_group = bpy.data.node_groups["TextureSettings_Advanced"]
if textureSettings.node_tree is not desired_group:
textureSettings.node_tree = desired_group
def setAutoProp(fieldProperty, pixelLength):
fieldProperty.mask = log2iRoundUp(pixelLength)
fieldProperty.shift = 0
fieldProperty.low = 0
fieldProperty.high = pixelLength
if fieldProperty.clamp and fieldProperty.mirror:
fieldProperty.high *= 2
fieldProperty.high -= 1
def set_texture_size(self, tex_size, tex_index):
nodes = self.node_tree.nodes
uv_basis: ShaderNodeGroup = nodes["UV Basis"]
inputs = uv_basis.inputs
inputs[f"{tex_index} S TexSize"].default_value = tex_size[0]
inputs[f"{tex_index} T TexSize"].default_value = tex_size[1]
def trunc_10_2(val: float):
return int(val * 4) / 4
def update_tex_values_field(self: Material, texProperty: "TextureProperty", tex_size: list[int], tex_index: int):
nodes = self.node_tree.nodes
textureSettings: ShaderNodeGroup = nodes["TextureSettings"]
inputs = textureSettings.inputs
set_texture_size(self, tex_size, tex_index)
if texProperty.autoprop:
setAutoProp(texProperty.S, tex_size[0])
setAutoProp(texProperty.T, tex_size[1])
str_index = str(tex_index)
# S/T Low
inputs[str_index + " S Low"].default_value = trunc_10_2(texProperty.S.low)
inputs[str_index + " T Low"].default_value = trunc_10_2(texProperty.T.low)
# S/T High
inputs[str_index + " S High"].default_value = trunc_10_2(texProperty.S.high)
inputs[str_index + " T High"].default_value = trunc_10_2(texProperty.T.high)
# Clamp
inputs[str_index + " ClampX"].default_value = 1 if texProperty.S.clamp else 0
inputs[str_index + " ClampY"].default_value = 1 if texProperty.T.clamp else 0
# Mask
inputs[str_index + " S Mask"].default_value = texProperty.S.mask
inputs[str_index + " T Mask"].default_value = texProperty.T.mask
# Mirror
inputs[str_index + " MirrorX"].default_value = 1 if texProperty.S.mirror > 0 else 0
inputs[str_index + " MirrorY"].default_value = 1 if texProperty.T.mirror > 0 else 0
# Shift
inputs[str_index + " S Shift"].default_value = texProperty.S.shift
inputs[str_index + " T Shift"].default_value = texProperty.T.shift
def iter_tex_nodes(node_tree: NodeTree, texIndex: int) -> Generator[TextureNodeImage, None, None]:
for i in range(1, 5):
nodeName = f"Tex{texIndex}_{i}"
if node_tree.nodes.get(nodeName):
yield node_tree.nodes[nodeName]
def toggle_texture_node_muting(material: Material, texIndex: int, isUsed: bool):
node_tree = material.node_tree
f3dMat: "F3DMaterialProperty" = material.f3d_mat
# Enforce typing from generator
texNode: None | TextureNodeImage = None
node_3point_key = "3 Point Lerp" if texIndex == 0 else "3 Point Lerp.001"
node_3point = node_tree.nodes.get(node_3point_key)
node_tex_color_conv_key = f"Tex{texIndex}_I"
node_tex_color_conv = node_tree.nodes.get(node_tex_color_conv_key)
# flip bool for clarity
shouldMute = not isUsed
for texNode in iter_tex_nodes(node_tree, texIndex):
if texNode.mute != shouldMute:
texNode.mute = shouldMute
if node_tex_color_conv and node_tex_color_conv.mute != shouldMute:
node_tex_color_conv.mute = shouldMute
mute_3point = shouldMute or f3dMat.rdp_settings.g_mdsft_text_filt != "G_TF_BILERP"
if node_3point and node_3point.mute != mute_3point:
node_3point.mute = mute_3point
def set_texture_nodes_settings(
material: Material, texProperty: "TextureProperty", texIndex: int, isUsed: bool
) -> list[int] | None:
node_tree = material.node_tree
f3dMat: "F3DMaterialProperty" = material.f3d_mat
# Return value
texSize: Optional[list[int]] = None
toggle_texture_node_muting(material, texIndex, isUsed)
if not isUsed:
return texSize
# Enforce typing from generator
texNode: None | TextureNodeImage = None
for texNode in iter_tex_nodes(node_tree, texIndex):
if texNode.image is not texProperty.tex:
texNode.image = texProperty.tex
texNode.interpolation = "Linear" if f3dMat.rdp_settings.g_mdsft_text_filt == "G_TF_AVERAGE" else "Closest"
if texSize:
continue
if texNode.image is not None or texProperty.use_tex_reference:
if texNode.image is not None:
texSize = texNode.image.size
else:
texSize = texProperty.tex_reference_size
return texSize
def update_tex_values_index(self: Material, *, texProperty: "TextureProperty", texIndex: int, isUsed: bool):
nodes = self.node_tree.nodes
tex_size = set_texture_nodes_settings(self, texProperty, texIndex, isUsed)
if tex_size: # only returns tex size if a texture is being set
if tex_size[0] > 0 and tex_size[1] > 0:
update_tex_values_field(self, texProperty, tex_size, texIndex)
texFormat = texProperty.tex_format
ciFormat = texProperty.ci_format
if has_f3d_nodes(self):
tex_I_node = nodes["Tex" + str(texIndex) + "_I"]
desired_node = bpy.data.node_groups["Is not i"]
if "IA" in texFormat or (texFormat[:2] == "CI" and "IA" in ciFormat):
desired_node = bpy.data.node_groups["Is ia"]
elif texFormat[0] == "I" or (texFormat[:2] == "CI" and ciFormat[0] == "I"):
desired_node = bpy.data.node_groups["Is i"]
if tex_I_node.node_tree is not desired_node:
tex_I_node.node_tree = desired_node
def get_color_info_from_tex(tex: bpy.types.Image):
is_greyscale, has_alpha_4_bit, has_alpha_1_bit = True, False, False
rgba_colors: set[int] = set()
pixels, channel_count = tex.pixels, tex.channels
for x in range(tex.size[0]):
for y in range(tex.size[1]): # N64 is -Y, Blender is +Y, in this context this doesn´t matter
pixel_color = [1, 1, 1, 1]
for field in range(channel_count):
pixel_color[field] = pixels[(y * tex.size[0] + x) * channel_count + field]
rgba_colors.add(getRGBA16Tuple(pixel_color))
if not (pixel_color[0] == pixel_color[1] and pixel_color[1] == pixel_color[2]):
is_greyscale = False
if pixel_color[3] < 0.9375:
has_alpha_4_bit = True
if pixel_color[3] < 0.5:
has_alpha_1_bit = True
return is_greyscale, has_alpha_1_bit, has_alpha_4_bit, rgba_colors
def get_optimal_format(tex: bpy.types.Image | None, prefer_rgba_over_ci: bool):
if not tex:
return "RGBA16"
n_size = tex.size[0] * tex.size[1]
if n_size > 8192: # Image is too big
return "RGBA16"
is_greyscale, has_alpha_1_bit, has_alpha_4_bit, rgba_colors = get_color_info_from_tex(tex)
if is_greyscale:
if n_size > 4096:
if has_alpha_1_bit:
return "IA4"
return "I4"
if has_alpha_4_bit:
return "IA8"
return "I8"
else:
if len(rgba_colors) <= 16 and (not prefer_rgba_over_ci or n_size > 2048):
return "CI4"
if not prefer_rgba_over_ci and len(rgba_colors) <= 256:
return "CI8"
return "RGBA16"
def update_tex_values_and_formats(self, context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
settings_props = context.scene.fast64.settings
if not settings_props.auto_pick_texture_format:
update_tex_values_manual(material, context)
return
f3d_mat: F3DMaterialProperty = material.f3d_mat
useDict = all_combiner_uses(f3d_mat)
tex0_props = f3d_mat.tex0
tex1_props = f3d_mat.tex1
tex0, tex1 = tex0_props.tex if useDict["Texture 0"] else None, (
tex1_props.tex if useDict["Texture 1"] else None
)
if tex0:
tex0_props.tex_format = get_optimal_format(tex0, settings_props.prefer_rgba_over_ci)
if tex1:
tex1_props.tex_format = get_optimal_format(tex1, settings_props.prefer_rgba_over_ci)
if tex0 and tex1:
if tex0_props.tex_format.startswith("CI") and not tex1_props.tex_format.startswith("CI"):
tex0_props.tex_format = "RGBA16"
elif tex1_props.tex_format.startswith("CI") and not tex0_props.tex_format.startswith("CI"):
tex1_props.tex_format = "RGBA16"
update_tex_values_manual(material, context)
def update_tex_values(self, context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
try:
prop_path = self.path_from_id()
except:
prop_path = None
update_tex_values_manual(material, context, prop_path=prop_path)
def get_tex_basis_size(f3d_mat: "F3DMaterialProperty"):
tex_size = None
if f3d_mat.tex0.tex is not None and f3d_mat.tex1.tex is not None:
return f3d_mat.tex0.tex.size if f3d_mat.uv_basis == "TEXEL0" else f3d_mat.tex1.tex.size
elif f3d_mat.tex0.tex is not None:
return f3d_mat.tex0.tex.size
elif f3d_mat.tex1.tex is not None:
return f3d_mat.tex1.tex.size
return tex_size
def get_tex_gen_size(tex_size: list[int | float]):
return (tex_size[0] - 1) / 1024, (tex_size[1] - 1) / 1024
def get_textlut_mode(f3d_mat: "F3DMaterialProperty", inherit_from_tex: bool = False):
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 []
tlut_modes = [tex.tlut_mode for tex in textures]
if tlut_modes and tlut_modes[0] == tlut_modes[-1]:
return tlut_modes[0]
return None if inherit_from_tex else f3d_mat.rdp_settings.g_mdsft_textlut
def update_tex_values_manual(material: Material, context, prop_path=None):
f3dMat: "F3DMaterialProperty" = material.f3d_mat
nodes = material.node_tree.nodes
texture_settings = nodes["TextureSettings"]
texture_inputs: NodeInputs = texture_settings.inputs
useDict = all_combiner_uses(f3dMat)
f3dMat.rdp_settings.g_mdsft_textlut = get_textlut_mode(f3dMat)
tex0_used = useDict["Texture 0"] and f3dMat.tex0.tex is not None
tex1_used = useDict["Texture 1"] and f3dMat.tex1.tex is not None
if not tex0_used and not tex1_used:
texture_settings.mute = True
set_texture_nodes_settings(material, f3dMat.tex0, 0, False)
set_texture_nodes_settings(material, f3dMat.tex1, 1, False)
return
elif texture_settings.mute:
texture_settings.mute = False
# linear requires tex gen to be enabled as well
isTexGen = f3dMat.rdp_settings.g_lighting and f3dMat.rdp_settings.g_tex_gen
if f3dMat.scale_autoprop:
if isTexGen:
tex_size = get_tex_basis_size(f3dMat)
if tex_size is not None:
# This is needed for exporting tex gen!
f3dMat.tex_scale = get_tex_gen_size(tex_size)
else:
f3dMat.tex_scale = (1, 1)
if f3dMat.tex0.tex is not None:
texture_inputs["0 S TexSize"].default_value = f3dMat.tex0.tex.size[0]
texture_inputs["0 T TexSize"].default_value = f3dMat.tex0.tex.size[0]
if f3dMat.tex1.tex is not None:
texture_inputs["1 S TexSize"].default_value = f3dMat.tex1.tex.size[0]
texture_inputs["1 T TexSize"].default_value = f3dMat.tex1.tex.size[0]
uv_basis: ShaderNodeGroup = nodes["UV Basis"]
if f3dMat.uv_basis == "TEXEL0":
uv_basis.node_tree = bpy.data.node_groups["UV Basis 0"]
else:
uv_basis.node_tree = bpy.data.node_groups["UV Basis 1"]
if not isTexGen:
uv_basis.inputs["S Scale"].default_value = f3dMat.tex_scale[0]
uv_basis.inputs["T Scale"].default_value = f3dMat.tex_scale[1]
elif f3dMat.scale_autoprop:
# Tex gen is 1:1
uv_basis.inputs["S Scale"].default_value = 1
uv_basis.inputs["T Scale"].default_value = 1
else:
gen_size = get_tex_gen_size(get_tex_basis_size(f3dMat))
# scale tex gen proportionally
node_uv_scale = (f3dMat.tex_scale[0] / gen_size[0], f3dMat.tex_scale[1] / gen_size[1])
uv_basis.inputs["S Scale"].default_value = node_uv_scale[0]
uv_basis.inputs["T Scale"].default_value = node_uv_scale[1]
if not prop_path or "tex0" in prop_path:
update_tex_values_index(material, texProperty=f3dMat.tex0, texIndex=0, isUsed=tex0_used)
if not prop_path or "tex1" in prop_path:
update_tex_values_index(material, texProperty=f3dMat.tex1, texIndex=1, isUsed=tex1_used)
texture_inputs["3 Point"].default_value = int(f3dMat.rdp_settings.g_mdsft_text_filt == "G_TF_BILERP")
uv_basis.inputs["EnableOffset"].default_value = int(f3dMat.rdp_settings.g_mdsft_text_filt != "G_TF_POINT")
set_texture_settings_node(material)
def shift_num(num: int, amt: int):
if amt < 0:
return num >> -amt
return num << amt
def shift_dimensions(tex_prop: "TextureProperty", dimensions: tuple[int, int]):
shifted = (shift_num(dimensions[0], tex_prop.S.shift), shift_num(dimensions[1], tex_prop.T.shift))
s_mirror_scale = 2 if tex_prop.S.mirror else 1
t_mirror_scale = 2 if tex_prop.T.mirror else 1
return (shifted[0] * s_mirror_scale, shifted[1] * t_mirror_scale)
def getMaterialScrollDimensions(f3dMat):
texDimensions0 = None
texDimensions1 = None
useDict = all_combiner_uses(f3dMat)
if useDict["Texture 0"] and f3dMat.tex0.tex_set:
if f3dMat.tex0.use_tex_reference:
texDimensions0 = f3dMat.tex0.tex_reference_size
elif f3dMat.tex0.tex:
texDimensions0 = (f3dMat.tex0.tex.size[0], f3dMat.tex0.tex.size[1])
if useDict["Texture 1"] and f3dMat.tex1.tex_set:
if f3dMat.tex1.use_tex_reference:
texDimensions1 = f3dMat.tex1.tex_reference_size
elif f3dMat.tex0.tex:
texDimensions1 = (f3dMat.tex1.tex.size[0], f3dMat.tex1.tex.size[1])
if texDimensions0 is not None:
texDimensions0 = shift_dimensions(f3dMat.tex0, texDimensions0)
else:
texDimensions0 = (1, 1)
if texDimensions1 is not None:
texDimensions1 = shift_dimensions(f3dMat.tex1, texDimensions1)
else:
texDimensions1 = (1, 1)
return (max(1, texDimensions0[0], texDimensions1[0]), max(1, texDimensions0[1], texDimensions1[1]))
def update_preset_manual(material, context):
if has_f3d_nodes(material):
update_node_values_of_material(material, context)
update_tex_values_manual(material, context)
def update_preset_manual_v4(material, preset):
if preset == "Shaded Solid":
preset = "sm64_shaded_solid"
if preset == "Shaded Texture":
preset = "sm64_shaded_texture"
if preset.lower() != "custom":
material_apply_preset(material, findF3DPresetPath(preset))
def material_apply_preset(material, filepath):
material.f3d_update_flag = True
with bpy.context.temp_override(material=material):
bpy.ops.script.execute_preset(filepath=filepath, menu_idname="MATERIAL_MT_f3d_presets")
# Since the material preset is executed under f3d_update_flag,
# it setting the rendermode presets does not propagate to the individual
# rendermode values.
# So manually call the function to propagate the preset.
# Also that function will set props, which results in the material preset name
# being set to Custom (as if changed by the user).
# So save the preset name and restore it after the rendermode propagation.
savedPresetName = material.f3d_mat.presetName
rendermode_preset_to_advanced(material)
material.f3d_mat.presetName = savedPresetName
material.f3d_update_flag = False
def has_f3d_nodes(material: Material):
return "Material Output F3D" in material.node_tree.nodes
@persistent
def load_handler(dummy):
logger.info("Checking for base F3D material library.")
for lib in bpy.data.libraries:
lib_path = bpy.path.abspath(lib.filepath)
# detect if this is one your addon's libraries here
if "f3d_material_library.blend" in lib_path:
addon_dir = os.path.dirname(os.path.abspath(__file__))
new_lib_path = os.path.join(addon_dir, "f3d_material_library.blend")
if lib_path != new_lib_path:
logger.info("Reloading the library: %s : %s => %s" % (lib.name, lib_path, new_lib_path))
lib.filepath = new_lib_path
lib.reload()
bpy.context.scene["f3d_lib_dir"] = None # force node reload!
link_f3d_material_library()
for mat in bpy.data.materials:
if mat is not None and mat.use_nodes and mat.is_f3d:
rendermode_preset_to_advanced(mat)
SCENE_PROPERTIES_VERSION = 2
def createOrUpdateSceneProperties():
group = bpy.data.node_groups.get("SceneProperties")
upgrade_group = bool(group and group.get("version", -1) < SCENE_PROPERTIES_VERSION)
if group and not upgrade_group:
# Group is ready and up to date
return
if upgrade_group and group:
# Need to upgrade; remove old outputs
if bpy.app.version >= (4, 0, 0):
for item in group.interface.items_tree:
if item.item_type == "SOCKET" and item.in_out == "OUTPUT":
group.interface.remove(item)
else:
for out in group.outputs:
group.outputs.remove(out)
new_group = group
else:
logger.info("Creating Scene Properties")
# create a group
new_group = bpy.data.node_groups.new("SceneProperties", "ShaderNodeTree")
# create group outputs
new_group.nodes.new("NodeGroupOutput")
new_group["version"] = SCENE_PROPERTIES_VERSION
# Create outputs
if bpy.app.version >= (4, 0, 0):
tree_interface = new_group.interface
_nodeFogEnable: NodeSocketFloat = tree_interface.new_socket(
"FogEnable", socket_type="NodeSocketFloat", in_out="OUTPUT"
)
_nodeFogColor: NodeSocketColor = tree_interface.new_socket(
"FogColor", socket_type="NodeSocketColor", in_out="OUTPUT"
)
_nodeF3D_NearClip: NodeSocketFloat = tree_interface.new_socket(
"F3D_NearClip", socket_type="NodeSocketFloat", in_out="OUTPUT"
)
_nodeF3D_FarClip: NodeSocketFloat = tree_interface.new_socket(
"F3D_FarClip", socket_type="NodeSocketFloat", in_out="OUTPUT"
)
_nodeBlender_Game_Scale: NodeSocketFloat = tree_interface.new_socket(
"Blender_Game_Scale", socket_type="NodeSocketFloat", in_out="OUTPUT"
)
_nodeFogNear: NodeSocketFloat = tree_interface.new_socket(
"FogNear", socket_type="NodeSocketFloat", in_out="OUTPUT"
)
_nodeFogFar: NodeSocketFloat = tree_interface.new_socket(
"FogFar", socket_type="NodeSocketFloat", in_out="OUTPUT"
)
_nodeAmbientColor: NodeSocketColor = tree_interface.new_socket(
"AmbientColor", socket_type="NodeSocketColor", in_out="OUTPUT"
)
_nodeLight0Color: NodeSocketColor = tree_interface.new_socket(
"Light0Color", socket_type="NodeSocketColor", in_out="OUTPUT"
)
_nodeLight0Dir: NodeSocketVector = tree_interface.new_socket(
"Light0Dir", socket_type="NodeSocketVector", in_out="OUTPUT"
)
_nodeLight0Size: NodeSocketFloat = tree_interface.new_socket(
"Light0Size", socket_type="NodeSocketFloat", in_out="OUTPUT"
)
_nodeLight1Color: NodeSocketColor = tree_interface.new_socket(
"Light1Color", socket_type="NodeSocketColor", in_out="OUTPUT"
)
_nodeLight1Dir: NodeSocketVector = tree_interface.new_socket(
"Light1Dir", socket_type="NodeSocketVector", in_out="OUTPUT"
)
_nodeLight1Size: NodeSocketFloat = tree_interface.new_socket(
"Light1Size", socket_type="NodeSocketFloat", in_out="OUTPUT"
)
else:
_nodeFogEnable: NodeSocketInt = new_group.outputs.new("NodeSocketInt", "FogEnable")
_nodeFogColor: NodeSocketColor = new_group.outputs.new("NodeSocketColor", "FogColor")
_nodeF3D_NearClip: NodeSocketFloat = new_group.outputs.new("NodeSocketFloat", "F3D_NearClip")
_nodeF3D_FarClip: NodeSocketFloat = new_group.outputs.new("NodeSocketFloat", "F3D_FarClip")
_nodeBlender_Game_Scale: NodeSocketFloat = new_group.outputs.new("NodeSocketFloat", "Blender_Game_Scale")
_nodeFogNear: NodeSocketInt = new_group.outputs.new("NodeSocketInt", "FogNear")
_nodeFogFar: NodeSocketInt = new_group.outputs.new("NodeSocketInt", "FogFar")
_nodeAmbientColor: NodeSocketColor = new_group.outputs.new("NodeSocketColor", "AmbientColor")
_nodeLight0Color: NodeSocketColor = new_group.outputs.new("NodeSocketColor", "Light0Color")
_nodeLight0Dir: NodeSocketVectorDirection = new_group.outputs.new("NodeSocketVectorDirection", "Light0Dir")
_nodeLight0Size: NodeSocketInt = new_group.outputs.new("NodeSocketInt", "Light0Size")
_nodeLight1Color: NodeSocketColor = new_group.outputs.new("NodeSocketColor", "Light1Color")
_nodeLight1Dir: NodeSocketVectorDirection = new_group.outputs.new("NodeSocketVectorDirection", "Light1Dir")
_nodeLight1Size: NodeSocketInt = new_group.outputs.new("NodeSocketInt", "Light1Size")
# Set outputs from render settings
sceneOutputs: NodeGroupOutput = new_group.nodes["Group Output"]
renderSettings: "Fast64RenderSettings_Properties" = bpy.context.scene.fast64.renderSettings
update_scene_props_from_render_settings(sceneOutputs, renderSettings)
def createScenePropertiesForMaterial(material: Material):
node_tree = material.node_tree
# Either create or update SceneProperties if needed
createOrUpdateSceneProperties()
# create a new group node to hold the tree
scene_props = node_tree.nodes.new(type="ShaderNodeGroup")
scene_props.name = "SceneProperties"
scene_props.location = (-320, -23)
scene_props.node_tree = bpy.data.node_groups["SceneProperties"]
# Fog links to reroutes and the CalcFog block
node_tree.links.new(scene_props.outputs["FogEnable"], node_tree.nodes["FogEnable"].inputs[0])
node_tree.links.new(scene_props.outputs["FogColor"], node_tree.nodes["FogColor"].inputs[0])
node_tree.links.new(scene_props.outputs["F3D_NearClip"], node_tree.nodes["CalcFog"].inputs["F3D_NearClip"])
node_tree.links.new(scene_props.outputs["F3D_FarClip"], node_tree.nodes["CalcFog"].inputs["F3D_FarClip"])
node_tree.links.new(
scene_props.outputs["Blender_Game_Scale"], node_tree.nodes["CalcFog"].inputs["Blender_Game_Scale"]
)
node_tree.links.new(scene_props.outputs["FogNear"], node_tree.nodes["CalcFog"].inputs["FogNear"])
node_tree.links.new(scene_props.outputs["FogFar"], node_tree.nodes["CalcFog"].inputs["FogFar"])
# Lighting links to reroutes. The colors are connected to other reroutes for update_light_colors,
# the others go directly to the Shade Color block.
node_tree.links.new(scene_props.outputs["AmbientColor"], node_tree.nodes["AmbientColor"].inputs[0])
node_tree.links.new(scene_props.outputs["Light0Color"], node_tree.nodes["Light0Color"].inputs[0])
node_tree.links.new(scene_props.outputs["Light0Dir"], node_tree.nodes["Light0Dir"].inputs[0])
node_tree.links.new(scene_props.outputs["Light0Size"], node_tree.nodes["Light0Size"].inputs[0])
node_tree.links.new(scene_props.outputs["Light1Color"], node_tree.nodes["Light1Color"].inputs[0])
node_tree.links.new(scene_props.outputs["Light1Dir"], node_tree.nodes["Light1Dir"].inputs[0])
node_tree.links.new(scene_props.outputs["Light1Size"], node_tree.nodes["Light1Size"].inputs[0])
def link_f3d_material_library():
dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "f3d_material_library.blend")
prevMode = bpy.context.mode
if prevMode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
with bpy.data.libraries.load(dir) as (data_from, data_to):
dirMat = os.path.join(dir, "Material")
dirNode = os.path.join(dir, "NodeTree")
for mat in data_from.materials:
if mat is not None:
bpy.ops.wm.link(filepath=os.path.join(dirMat, mat), directory=dirMat, filename=mat)
# linking is SUPER slow, this only links if the scene hasnt been linked yet
# in future updates, this will likely need to be something numerated so if more nodes are added then they will be linked
if bpy.context.scene.get("f3d_lib_dir") != dirNode:
# link groups after to bring extra node_groups
for node_group in data_from.node_groups:
if node_group is not None:
bpy.ops.wm.link(filepath=os.path.join(dirNode, node_group), directory=dirNode, filename=node_group)
bpy.context.scene["f3d_lib_dir"] = dirNode
# TODO: Figure out a better way to save the user's old mode
if prevMode != "OBJECT":
bpy.ops.object.mode_set(mode=get_mode_set_from_context_mode(prevMode))
def get_f3d_node_tree() -> bpy.types.NodeTree:
try:
link_f3d_material_library()
mat = bpy.data.materials["fast64_f3d_material_library_beefwashere"]
return mat.node_tree.copy()
finally:
bpy.data.materials.remove(mat)
def shouldConvOrCreateColorAttribute(mesh: Mesh, attr_name="Col"):
has_attr, conv_attr = False, False
if attr_name in mesh.attributes:
attribute: Attribute = mesh.attributes[attr_name]
has_attr = True
conv_attr = attribute.data_type != "FLOAT_COLOR" or attribute.domain != "CORNER"
return has_attr, conv_attr
def convertColorAttribute(mesh: Mesh, attr_name="Col"):
prev_index = mesh.attributes.active_index
attr_index = mesh.attributes.find(attr_name)
if attr_index < 0:
raise PluginError(f"Failed to find the index for mesh attr {attr_name}. Attribute conversion has failed!")
mesh.attributes.active_index = attr_index
bpy.ops.geometry.attribute_convert(mode="GENERIC", domain="CORNER", data_type="FLOAT_COLOR")
mesh.attributes.active_index = prev_index
def addColorAttributesToModel(obj: Object):
if obj.type != "MESH":
return
prevMode = bpy.context.mode
if prevMode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
selectSingleObject(obj)
mesh: Mesh = obj.data
conv_col, has_col = shouldConvOrCreateColorAttribute(mesh, attr_name="Col")
if conv_col:
convertColorAttribute(mesh, attr_name="Col")
elif not has_col:
mesh.color_attributes.new("Col", "FLOAT_COLOR", "CORNER")
conv_alpha, has_alpha = shouldConvOrCreateColorAttribute(mesh, attr_name="Alpha")
if conv_alpha:
convertColorAttribute(mesh, attr_name="Alpha")
elif not has_alpha:
mesh.color_attributes.new("Alpha", "FLOAT_COLOR", "CORNER")
if prevMode != "OBJECT":
bpy.ops.object.mode_set(mode=get_mode_set_from_context_mode(prevMode))
def add_f3d_mat_to_obj(obj: bpy.types.Object, material, index=None):
# add material to object
if obj is not None:
addColorAttributesToModel(obj)
if index is None:
obj.data.materials.append(material)
if bpy.context.object is not None:
bpy.context.object.active_material_index = len(obj.material_slots) - 1
else:
obj.material_slots[index].material = material
if bpy.context.object is not None:
bpy.context.object.active_material_index = index
def createF3DMat(obj: Object | None, preset="Shaded Solid", index=None):
# link all node_groups + material from addon's data .blend
link_f3d_material_library()
# a linked material containing the default layout for all the linked node_groups
mat = bpy.data.materials["fast64_f3d_material_library_beefwashere"]
# duplicate and rename the linked material
material = mat.copy()
material.name = "f3dlite_material"
# remove the linked material so it doesn't bother anyone or get meddled with
bpy.data.materials.remove(mat)
createScenePropertiesForMaterial(material)
add_f3d_mat_to_obj(obj, material, index)
material.is_f3d = True
material.mat_ver = F3D_MAT_CUR_VERSION
update_preset_manual_v4(material, preset)
return material
def reloadDefaultF3DPresets():
presetNameToFilename = {}
for _, gamePresets in material_presets.items():
for presetName, _ in gamePresets.items():
presetNameToFilename[bpy.path.display_name(presetName)] = presetName
for material in bpy.data.materials:
if material.f3d_mat.presetName in presetNameToFilename:
update_preset_manual_v4(material, presetNameToFilename[material.f3d_mat.presetName])
def check_or_ask_color_management(context: Context):
scene = context.scene
fast64_props: "Fast64_Properties" = scene.fast64
fast64settings_props: "Fast64Settings_Properties" = fast64_props.settings
view_settings = scene.view_settings
# Check if color management settings are correct
if not fast64settings_props.dont_ask_color_management and (
scene.display_settings.display_device != "sRGB"
or view_settings.view_transform != "Standard"
or view_settings.look != "None"
or view_settings.exposure != 0.0
or view_settings.gamma != 1.0
):
bpy.ops.dialog.fast64_update_color_management("INVOKE_DEFAULT")
class UpdateColorManagementPopup(Operator):
bl_label = "Update Color Management"
bl_idname = "dialog.fast64_update_color_management"
bl_description = "Update color management settings to help material preview accuracy"
bl_options = {"UNDO"}
already_invoked = False # HACK: used to prevent multiple dialogs from popping up
def invoke(self, context: Context, event: Event):
if UpdateColorManagementPopup.already_invoked:
return {"FINISHED"}
UpdateColorManagementPopup.already_invoked = True
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context: Context):
col = self.layout.column()
multilineLabel(
col,
(
(
f'The color management settings for the current scene "{context.scene.name}"\n'
# use is_library_indirect to not count the scene from f3d_material_library.blend and other "external" scenes
if len([_s for _s in bpy.data.scenes if not _s.is_library_indirect]) >= 2
else "The color management settings\n"
)
+ "will cause inaccurate preview compared to N64.\n"
+ "Would you like to update it?"
),
icon="INFO",
)
fast64_props: "Fast64_Properties" = context.scene.fast64
fast64settings_props: "Fast64Settings_Properties" = fast64_props.settings
col.prop(fast64settings_props, "dont_ask_color_management", text="Don't ask again")
def cancel(self, context: Context):
UpdateColorManagementPopup.already_invoked = False
def execute(self, context):
try:
scene = context.scene
scene.display_settings.display_device = "sRGB"
scene.view_settings.view_transform = "Standard"
scene.view_settings.look = "None"
scene.view_settings.exposure = 0.0
scene.view_settings.gamma = 1.0
self.report({"INFO"}, "Updated color management settings.")
return {"FINISHED"}
except Exception as exc:
raisePluginError(self, exc)
return {"CANCELLED"}
finally:
UpdateColorManagementPopup.already_invoked = False
class RecreateF3DNodes(Operator):
bl_idname = "material.recreate_f3d_nodes"
bl_label = "Recreate F3D Shader Nodes"
bl_options = {"REGISTER", "UNDO", "PRESET"}
bl_description = "Recreates the node tree for f3d materials, use if material preview is broken."
def execute(self, context):
material = context.material
if material is None:
self.report({"ERROR"}, "No active material.")
else:
node_tree_copy(get_f3d_node_tree(), material.node_tree)
createScenePropertiesForMaterial(material)
update_all_node_values(material, context)
self.report({"INFO"}, "Success!")
return {"FINISHED"}
class CreateFast3DMaterial(Operator):
bl_idname = "object.create_f3d_mat"
bl_label = "Create Fast3D Material"
bl_options = {"REGISTER", "UNDO", "PRESET"}
def execute(self, context):
obj = bpy.context.view_layer.objects.active
if obj is None:
self.report({"ERROR"}, "No active object selected.")
else:
preset = getDefaultMaterialPreset("Shaded Solid")
createF3DMat(obj, preset)
self.report({"INFO"}, "Created new Fast3D material.")
return {"FINISHED"}
class ReloadDefaultF3DPresets(Operator):
bl_idname = "object.reload_f3d_presets"
bl_label = "Reload Default Fast3D Presets"
bl_options = {"REGISTER", "UNDO", "PRESET"}
def execute(self, context):
reloadDefaultF3DPresets()
self.report({"INFO"}, "Success!")
return {"FINISHED"}
def get_tex_prop_from_path(material: Material, path: str) -> Tuple["TextureProperty", int]:
if "tex0" in path:
return material.f3d_mat.tex0, 0
return material.f3d_mat.tex1, 1
def already_updating_material(material: Material | None):
"""Check if material is updating already"""
return getattr(material, "f3d_update_flag", False)
def update_tex_field_prop(self: Property, context: Context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
prop_path = self.path_from_id()
tex_property, tex_index = get_tex_prop_from_path(material, prop_path)
tex_size = tex_property.get_tex_size()
if tex_size[0] > 0 and tex_size[1] > 0:
update_tex_values_field(material, tex_property, tex_size, tex_index)
set_texture_settings_node(material)
def toggle_auto_prop(self, context: Context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
prop_path = self.path_from_id()
tex_property, tex_index = get_tex_prop_from_path(material, prop_path)
if tex_property.autoprop:
tex_size = tuple([s for s in tex_property.get_tex_size()])
if tex_size[0] > 0 and tex_size[1] > 0:
update_tex_values_field(material, tex_property, tex_size, tex_index)
set_texture_settings_node(material)
class TextureFieldProperty(PropertyGroup):
clamp: bpy.props.BoolProperty(
name="Clamp",
update=update_tex_field_prop,
)
mirror: bpy.props.BoolProperty(
name="Mirror",
update=update_tex_field_prop,
)
low: bpy.props.FloatProperty(
name="Low",
min=0,
max=1023.75,
update=update_tex_field_prop,
)
high: bpy.props.FloatProperty(
name="High",
min=0,
max=1023.75,
update=update_tex_field_prop,
)
mask: bpy.props.IntProperty(
name="Mask",
min=0,
max=15,
default=5,
update=update_tex_field_prop,
)
shift: bpy.props.IntProperty(
name="Shift",
min=-5,
max=10,
update=update_tex_field_prop,
)
def key(self):
return (self.clamp, self.mirror, round(self.low * 4), round(self.high * 4), self.mask, self.shift)
class SetTileSizeScrollProperty(PropertyGroup):
s: bpy.props.IntProperty(min=-4095, max=4095, default=0)
t: bpy.props.IntProperty(min=-4095, max=4095, default=0)
interval: bpy.props.IntProperty(min=1, soft_max=1000, default=1)
def key(self):
return (self.s, self.t, self.interval)
class TextureProperty(PropertyGroup):
tex: bpy.props.PointerProperty(
type=Image,
name="Texture",
update=update_tex_values_and_formats,
)
tex_format: bpy.props.EnumProperty(
name="Format",
items=enumTexFormat,
default="RGBA16",
update=update_tex_values,
)
ci_format: bpy.props.EnumProperty(
name="CI Format",
items=enumCIFormat,
default="RGBA16",
update=update_tex_values,
)
S: bpy.props.PointerProperty(type=TextureFieldProperty)
T: bpy.props.PointerProperty(type=TextureFieldProperty)
use_tex_reference: bpy.props.BoolProperty(
name="Use Texture Reference",
default=False,
update=update_tex_values,
)
tex_reference: bpy.props.StringProperty(
name="Texture Reference",
default="0x08000000",
)
tex_reference_size: bpy.props.IntVectorProperty(
name="Texture Reference Size",
min=1,
size=2,
default=(32, 32),
update=update_tex_values,
)
pal_reference: bpy.props.StringProperty(
name="Palette Reference",
default="0x08000000",
)
pal_reference_size: bpy.props.IntProperty(
name="Palette Reference Size",
min=1,
default=16,
)
menu: bpy.props.BoolProperty()
tex_set: bpy.props.BoolProperty(
default=True,
update=update_node_values_with_preset,
)
autoprop: bpy.props.BoolProperty(
name="Autoprop",
update=toggle_auto_prop,
default=True,
)
tile_scroll: bpy.props.PointerProperty(type=SetTileSizeScrollProperty)
@property
def is_ci(self):
self.tex_format: str
return self.tex_format.startswith("CI")
@property
def tlut_mode(self):
return f"G_TT_{self.ci_format if self.is_ci else 'NONE'}"
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
else:
return self.tex_reference_size
return [0, 0]
def key(self):
texSet = self.tex_set
isCI = self.tex_format == "CI8" or self.tex_format == "CI4"
useRef = self.use_tex_reference
return (
self.tex_set,
self.tex if texSet else None,
self.tex_format if texSet else None,
self.ci_format if texSet and isCI else None,
self.S.key() if texSet else None,
self.T.key() if texSet else None,
self.autoprop if texSet else None,
self.tile_scroll.key() if texSet else None,
self.use_tex_reference if texSet else None,
self.tex_reference if texSet and useRef else None,
self.tex_reference_size if texSet and useRef else None,
self.pal_reference if texSet and useRef and isCI else None,
self.pal_reference_size if texSet and useRef and isCI else None,
)
def on_tex_autoprop(texProperty, context):
if texProperty.autoprop and texProperty.tex is not None:
tex_size = texProperty.tex.size
if tex_size[0] > 0 and tex_size[1] > 0:
setAutoProp(texProperty.S, tex_size[0])
setAutoProp(texProperty.T, tex_size[1])
def update_combiner_connections_and_preset(self, context: Context):
with F3DMaterial_UpdateLock(get_material_from_context(context)) as material:
if not material:
return
f3d_mat: "F3DMaterialProperty" = material.f3d_mat
f3d_mat.presetName = "Custom"
prop_path = self.path_from_id()
combiner = 1 if "combiner1" in prop_path else 2
update_combiner_connections(material, context, combiner=combiner)
toggle_texture_node_muting(material, 0, f3d_mat.tex0.tex and combiner_uses_tex0(material.f3d_mat))
toggle_texture_node_muting(material, 1, f3d_mat.tex1.tex and combiner_uses_tex1(material.f3d_mat))
def ui_image(
canUseLargeTextures: bool,
layout: UILayout,
material: Material,
textureProp: TextureProperty,
name: str,
showCheckBox: bool,
hide_lowhigh=False,
):
inputGroup = layout.box().column()
inputGroup.prop(
textureProp, "menu", text=name + " Properties", icon="TRIA_DOWN" if textureProp.menu else "TRIA_RIGHT"
)
if textureProp.menu:
tex = textureProp.tex
prop_input_name = inputGroup.column()
prop_input = inputGroup.column()
if showCheckBox:
prop_input_name.prop(textureProp, "tex_set", text="Set Texture")
else:
prop_input_name.label(text=name)
texIndex = name[-1]
prop_input.prop(textureProp, "use_tex_reference")
if textureProp.use_tex_reference:
prop_split(prop_input, textureProp, "tex_reference", "Texture Reference")
prop_split(prop_input, textureProp, "tex_reference_size", "Texture Size")
if textureProp.tex_format[:2] == "CI":
flipbook = getattr(material.flipbookGroup, "flipbook" + texIndex)
if flipbook is None or not flipbook.enable:
prop_split(prop_input, textureProp, "pal_reference", "Palette Reference")
prop_split(prop_input, textureProp, "pal_reference_size", "Palette Size")
else:
prop_input.template_ID(
textureProp, "tex", new="image.new", open="image.open", unlink="image.tex" + texIndex + "_unlink"
)
prop_input.enabled = textureProp.tex_set
if tex is not None:
prop_input.label(text="Size: " + str(tex.size[0]) + " x " + str(tex.size[1]))
if textureProp.use_tex_reference:
width, height = textureProp.tex_reference_size[0], textureProp.tex_reference_size[1]
elif tex is not None:
width, height = tex.size[0], tex.size[1]
else:
width = height = 0
if canUseLargeTextures:
availTmem = 512
if textureProp.tex_format[:2] == "CI":
availTmem /= 2
useDict = all_combiner_uses(material.f3d_mat)
if useDict["Texture 0"] and useDict["Texture 1"]:
availTmem /= 2
isLarge = getTmemWordUsage(textureProp.tex_format, width, height) > availTmem
else:
isLarge = False
if isLarge:
msg = prop_input.box().column()
msg.label(text="This is a large texture.", icon="INFO")
msg.label(text="Recommend using Create Large Texture Mesh tool.")
else:
tmemUsageUI(prop_input, textureProp)
prop_split(prop_input, textureProp, "tex_format", name="Format")
if textureProp.tex_format[:2] == "CI":
prop_split(prop_input, textureProp, "ci_format", name="CI Format")
if not isLarge:
if width > 0 and height > 0:
texelsPerWord = 64 // texBitSizeInt[textureProp.tex_format]
if width % texelsPerWord != 0:
msg = prop_input.box().column()
msg.label(text=f"Suggest {textureProp.tex_format} tex be multiple ", icon="INFO")
msg.label(text=f"of {texelsPerWord} pixels wide for fast loading.")
warnClampS = (
not isPowerOf2(width)
and not textureProp.S.clamp
and (not textureProp.autoprop or textureProp.S.mask != 0)
)
warnClampT = (
not isPowerOf2(height)
and not textureProp.T.clamp
and (not textureProp.autoprop or textureProp.T.mask != 0)
)
if warnClampS or warnClampT:
msg = prop_input.box().column()
msg.label(text=f"Clamping required for non-power-of-2 image", icon="ERROR")
msg.label(text=f"dimensions. Enable clamp or set mask to 0.")
texFieldSettings = prop_input.column()
clampSettings = texFieldSettings.row()
clampSettings.prop(textureProp.S, "clamp", text="Clamp S")
clampSettings.prop(textureProp.T, "clamp", text="Clamp T")
mirrorSettings = texFieldSettings.row()
mirrorSettings.prop(textureProp.S, "mirror", text="Mirror S")
mirrorSettings.prop(textureProp.T, "mirror", text="Mirror T")
prop_input.prop(textureProp, "autoprop", text="Auto Set Other Properties")
if not textureProp.autoprop:
mask = prop_input.row()
mask.prop(textureProp.S, "mask", text="Mask S")
mask.prop(textureProp.T, "mask", text="Mask T")
shift = prop_input.row()
shift.prop(textureProp.S, "shift", text="Shift S")
shift.prop(textureProp.T, "shift", text="Shift T")
if hide_lowhigh:
return
low = prop_input.row()
low.prop(textureProp.S, "low", text="S Low")
low.prop(textureProp.T, "low", text="T Low")
high = prop_input.row()
high.prop(textureProp.S, "high", text="S High")
high.prop(textureProp.T, "high", text="T High")
class CombinerProperty(PropertyGroup):
A: bpy.props.EnumProperty(
name="A",
description="A",
items=combiner_enums["Case A"],
default="TEXEL0",
update=update_combiner_connections_and_preset,
)
B: bpy.props.EnumProperty(
name="B",
description="B",
items=combiner_enums["Case B"],
default="0",
update=update_combiner_connections_and_preset,
)
C: bpy.props.EnumProperty(
name="C",
description="C",
items=combiner_enums["Case C"],
default="SHADE",
update=update_combiner_connections_and_preset,
)
D: bpy.props.EnumProperty(
name="D",
description="D",
items=combiner_enums["Case D"],
default="0",
update=update_combiner_connections_and_preset,
)
A_alpha: bpy.props.EnumProperty(
name="A Alpha",
description="A Alpha",
items=combiner_enums["Case A Alpha"],
default="0",
update=update_combiner_connections_and_preset,
)
B_alpha: bpy.props.EnumProperty(
name="B Alpha",
description="B Alpha",
items=combiner_enums["Case B Alpha"],
default="0",
update=update_combiner_connections_and_preset,
)
C_alpha: bpy.props.EnumProperty(
name="C Alpha",
description="C Alpha",
items=combiner_enums["Case C Alpha"],
default="0",
update=update_combiner_connections_and_preset,
)
D_alpha: bpy.props.EnumProperty(
name="D Alpha",
description="D Alpha",
items=combiner_enums["Case D Alpha"],
default="ENVIRONMENT",
update=update_combiner_connections_and_preset,
)
def key(self):
return (
self.A,
self.B,
self.C,
self.D,
self.A_alpha,
self.B_alpha,
self.C_alpha,
self.D_alpha,
)
class ProceduralAnimProperty(PropertyGroup):
speed: bpy.props.FloatProperty(name="Speed", default=1)
amplitude: bpy.props.FloatProperty(name="Amplitude", default=1)
frequency: bpy.props.FloatProperty(name="Frequency", default=1)
spaceFrequency: bpy.props.FloatProperty(name="Space Frequency", default=0)
offset: bpy.props.FloatProperty(name="Offset", default=0)
noiseAmplitude: bpy.props.FloatProperty(name="Amplitude", default=1)
animate: bpy.props.BoolProperty()
animType: bpy.props.EnumProperty(name="Type", items=enumTexScroll)
def key(self):
anim = self.animate
return (
self.animate,
round(self.speed, 4) if anim else None,
round(self.amplitude, 4) if anim else None,
round(self.frequency, 4) if anim else None,
round(self.spaceFrequency, 4) if anim else None,
round(self.offset, 4) if anim else None,
round(self.noiseAmplitude, 4) if anim else None,
self.animType if anim else None,
)
class ProcAnimVectorProperty(PropertyGroup):
x: bpy.props.PointerProperty(type=ProceduralAnimProperty)
y: bpy.props.PointerProperty(type=ProceduralAnimProperty)
z: bpy.props.PointerProperty(type=ProceduralAnimProperty)
pivot: bpy.props.FloatVectorProperty(size=2, name="Pivot")
angularSpeed: bpy.props.FloatProperty(default=1, name="Angular Speed")
menu: bpy.props.BoolProperty()
def key(self):
return (
self.x.key(),
self.y.key(),
self.z.key(),
round(self.pivot[0], 4),
round(self.pivot[1], 4),
round(self.angularSpeed, 4),
)
class PrimDepthSettings(PropertyGroup):
z: bpy.props.IntProperty(
name="Prim Depth: Z",
default=0,
soft_min=-1,
soft_max=0x7FFF,
description=(
"""The value to use for z is the screen Z position of the object you are rendering."""
""" This is a value ranging from 0x0000 to 0x7fff, where 0x0000 usually corresponds to """
"""the near clipping plane and 0x7fff usually corresponds to the far clipping plane."""
""" You can use -1 to force Z to be at the far clipping plane."""
),
)
dz: bpy.props.IntProperty(
name="Prim Depth: Delta Z",
default=0,
soft_min=0,
soft_max=0x4000,
description=(
"""The dz value should be set to 0."""
""" This value is used for antialiasing and objects drawn in decal render mode """
"""and must always be a power of 2 (0, 1, 2, 4, 8, ... 0x4000)."""
""" If you are using decal mode and part of the decaled object is not being rendered correctly, """
"""try setting this to powers of 2. Otherwise use 0."""
),
)
def to_dict(self):
return prop_group_to_json(self)
def from_dict(self, data: dict):
json_to_prop_group(self, data)
def key(self):
return frozenset(self.to_dict().items())
class RDPSettings(PropertyGroup):
g_zbuffer: bpy.props.BoolProperty(
name="Z Buffer",
default=False,
update=update_node_values_with_preset,
description="Enables calculation of Z value for primitives. Disable if not reading or writing Z-Buffer in the blender",
)
g_shade: bpy.props.BoolProperty(
name="Shading",
default=False,
update=update_node_values_with_preset,
description="Computes shade coordinates for primitives. Disable if not using lighting, vertex colors or fog",
)
g_ambocclusion: bpy.props.BoolProperty(
name="Ambient Occlusion",
default=False,
update=update_node_values_with_preset,
description="F3DEX3: Scales each type light intensity differently with vertex alpha. Bake scene shadows / AO into vertex alpha, not vertex color",
)
g_attroffset_z_enable: bpy.props.BoolProperty(
name="Z Offset (for decal fix)",
default=False,
update=update_node_values_with_preset,
description="F3DEX3: Enables offset to vertex Z. To fix decals, set the Z mode to opaque and enable this",
)
g_attroffset_st_enable: bpy.props.BoolProperty(
name="ST Offset (for UV scroll)",
default=False,
update=update_node_values_with_preset,
description="F3DEX3: Enables offsets to vertex ST values, usually for UV scrolling",
)
# v1/2 difference
g_cull_front: bpy.props.BoolProperty(
name="Cull Front",
default=False,
update=update_node_values_with_preset,
description="Disables drawing of front faces",
)
# v1/2 difference
g_cull_back: bpy.props.BoolProperty(
name="Cull Back",
default=False,
update=update_node_values_with_preset,
description="Disables drawing of back faces",
)
g_packed_normals: bpy.props.BoolProperty(
name="Packed Normals (Vtx Colors + Lighting)",
default=False,
update=update_node_values_with_preset,
description="F3DEX3: Packs vertex normals in unused 16 bits of each vertex, enabling simultaneous vertex colors and lighting",
)
g_lighttoalpha: bpy.props.BoolProperty(
name="Light to Alpha (for cel shading)",
default=False,
update=update_node_values_with_preset,
description="F3DEX3: Moves light intensity to shade alpha, used for cel shading and other effects",
)
g_lighting_specular: bpy.props.BoolProperty(
name="Specular Lighting",
default=False,
update=update_node_values_with_preset,
description="F3DEX3: Microcode lighting computes specular instead of diffuse component. If using, must set size field of every light in code",
)
g_fresnel_color: bpy.props.BoolProperty(
name="Fresnel to Color",
default=False,
update=update_node_values_with_preset,
description="F3DEX3: Shade color derived from how much each vertex normal faces the camera. For bump mapping",
)
g_fresnel_alpha: bpy.props.BoolProperty(
name="Fresnel to Alpha",
default=False,
update=update_node_values_with_preset,
description="F3DEX3: Shade alpha derived from how much each vertex normal faces the camera. For water, glass, ghosts, etc., or toon outlines",
)
g_fog: bpy.props.BoolProperty(
name="Fog",
default=False,
update=update_node_values_with_preset,
description="Turns on/off fog calculation. Fog variable gets stored into shade alpha",
)
g_lighting: bpy.props.BoolProperty(
name="Lighting",
default=False,
update=update_node_values_with_preset,
description="Enables calculating shade color using lights. Turn off for vertex colors as shade color",
)
g_tex_gen: bpy.props.BoolProperty(
name="Texture UV Generate",
default=False,
update=update_node_values_with_preset,
description="Generates texture coordinates for reflection mapping based on vertex normals and lookat direction. On a skybox texture, maps the sky to the center of the texture and the ground to a circle inscribed in the border. Requires lighting enabled to use",
)
g_tex_gen_linear: bpy.props.BoolProperty(
name="Texture UV Generate Linear",
default=False,
update=update_node_values_with_preset,
description="Modifies the texgen mapping; enable with texgen. Use a normal panorama image for the texture, with the sky at the top and the ground at the bottom. Requires lighting enabled to use",
)
g_lod: bpy.props.BoolProperty(
name="LoD (does nothing)",
default=False,
update=update_node_values_with_preset,
description="Not implemented in any known microcodes. No effect whether enabled or disabled",
)
g_shade_smooth: bpy.props.BoolProperty(
name="Smooth Shading",
default=False,
update=update_node_values_with_preset,
description="Shades primitive smoothly using interpolation between shade values for each vertex (Gouraud shading)",
)
g_clipping: bpy.props.BoolProperty(
name="Clipping",
default=True,
update=update_node_values_with_preset,
description="F3DEX1/LX only, exact function unknown",
)
# upper half mode
# v2 only
g_mdsft_alpha_dither: bpy.props.EnumProperty(
name="Alpha Dither",
items=enumAlphaDither,
default="G_AD_DISABLE",
update=update_node_values_with_preset,
description="Applies your choice dithering type to output framebuffer alpha. Dithering is used to convert high precision source colors into lower precision framebuffer values",
)
# v2 only
g_mdsft_rgb_dither: bpy.props.EnumProperty(
name="RGB Dither",
items=enumRGBDither,
default="G_CD_MAGICSQ",
update=update_node_values_with_preset,
description="Applies your choice dithering type to output framebuffer color. Dithering is used to convert high precision source colors into lower precision framebuffer values",
)
g_mdsft_combkey: bpy.props.EnumProperty(
name="Chroma Key",
items=enumCombKey,
default="G_CK_NONE",
update=update_node_values_with_preset,
description="Turns on/off the chroma key. Chroma key requires a special setup to work properly",
)
g_mdsft_textconv: bpy.props.EnumProperty(
name="Texture Convert",
items=enumTextConv,
default="G_TC_CONV",
update=update_node_values_with_preset,
description="Sets the function of the texture convert unit, to do texture filtering, YUV to RGB conversion, or both",
)
g_mdsft_text_filt: bpy.props.EnumProperty(
name="Texture Filter",
items=enumTextFilt,
default="G_TF_POINT",
update=update_node_values_without_preset,
description="Applies your choice of filtering to texels",
)
g_mdsft_textlut: bpy.props.EnumProperty(
name="Texture LUT",
items=enumTextLUT,
default="G_TT_NONE",
description="Changes texture look up table (LUT) behavior. This property is auto set if you choose a CI texture",
)
g_mdsft_textlod: bpy.props.EnumProperty(
name="Texture LOD",
items=enumTextLOD,
default="G_TL_TILE",
update=update_node_values_with_preset,
description="Turns on/off the use of LoD on textures. LoD textures change the used tile based on the texel/pixel ratio",
)
num_textures_mipmapped: bpy.props.IntProperty(
name="Number of Mipmaps",
default=2,
min=2,
max=8,
description="Number of mipmaps when Texture LOD set to `LOD`. First cycle combiner should be ((Tex1 - Tex0) * LOD Frac) + Tex0",
)
g_mdsft_textdetail: bpy.props.EnumProperty(
name="Texture Detail",
items=enumTextDetail,
default="G_TD_CLAMP",
update=update_node_values_with_preset,
description="Changes type of LoD usage. Affects how tiles are selected based on texel magnification. Only works when G_TL_LOD is selected",
)
g_mdsft_textpersp: bpy.props.EnumProperty(
name="Texture Perspective Correction",
items=enumTextPersp,
default="G_TP_NONE",
update=update_node_values_with_preset,
description="Turns on/off texture perspective correction",
)
g_mdsft_cycletype: bpy.props.EnumProperty(
name="Cycle Type",
items=enumCycleType,
default="G_CYC_1CYCLE",
update=update_node_values_with_preset,
description="Changes RDP pipeline configuration. For normal textured triangles use one or two cycle mode",
)
# v1 only
g_mdsft_color_dither: bpy.props.EnumProperty(
name="Color Dither",
items=enumColorDither,
default="G_CD_ENABLE",
update=update_node_values_with_preset,
description="Applies your choice dithering type to output frambuffer",
)
g_mdsft_pipeline: bpy.props.EnumProperty(
name="Pipeline Span Buffer Coherency",
items=enumPipelineMode,
default="G_PM_NPRIMITIVE",
update=update_node_values_with_preset,
description="Changes primitive rasterization timing by adding syncs after tri draws. Vanilla SM64 has synchronization issues which could cause a crash if not using 1 prim. For any modern SM64 hacking project or other game N-prim should always be used",
)
# lower half mode
g_mdsft_alpha_compare: bpy.props.EnumProperty(
name="Alpha Compare",
items=enumAlphaCompare,
default="G_AC_NONE",
update=update_node_values_with_preset,
description="Uses alpha comparisons to decide if a pixel should be written. Applies before blending",
)
g_mdsft_zsrcsel: bpy.props.EnumProperty(
name="Z Source Selection",
items=enumDepthSource,
default="G_ZS_PIXEL",
update=update_node_values_with_preset,
description="Changes screen-space Z value source used for Z-Buffer calculations",
)
prim_depth: bpy.props.PointerProperty(
type=PrimDepthSettings,
name="Prim Depth Settings (gDPSetPrimDepth)",
description="gDPSetPrimDepth",
)
clip_ratio: bpy.props.IntProperty(
default=1,
min=1,
max=2**15 - 1,
update=update_node_values_with_preset,
)
# cycle independent
set_rendermode: bpy.props.BoolProperty(
default=False,
update=update_node_values_with_preset,
)
rendermode_advanced_enabled: bpy.props.BoolProperty(
default=False,
update=update_node_values_with_preset,
)
rendermode_preset_cycle_1: bpy.props.EnumProperty(
items=enumRenderModesCycle1,
default="G_RM_AA_ZB_OPA_SURF",
name="Render Mode Cycle 1",
update=update_rendermode_preset,
)
rendermode_preset_cycle_2: bpy.props.EnumProperty(
items=enumRenderModesCycle2,
default="G_RM_AA_ZB_OPA_SURF2",
name="Render Mode Cycle 2",
update=update_rendermode_preset,
)
aa_en: bpy.props.BoolProperty(
update=update_node_values_with_preset,
description="Enables anti-aliasing to rasterized primitive edges. Uses coverage to determine edges",
)
z_cmp: bpy.props.BoolProperty(
update=update_node_values_with_preset, description="Checks pixel Z value against Z-Buffer to test writing"
)
z_upd: bpy.props.BoolProperty(
update=update_node_values_with_preset,
description="Updates the Z-Buffer with the most recently written pixel Z value",
)
im_rd: bpy.props.BoolProperty(
update=update_node_values_with_preset, description="Enables reading from framebuffer for blending calculations"
)
clr_on_cvg: bpy.props.BoolProperty(
update=update_node_values_with_preset,
description="Only draw on coverage (amount primitive covers target pixel) overflow",
)
cvg_dst: bpy.props.EnumProperty(
name="Coverage Destination",
items=enumCoverage,
update=update_node_values_with_preset,
description="Changes how coverage (amount primitive covers target pixel) gets retrieved/stored",
)
zmode: bpy.props.EnumProperty(
name="Z Mode",
items=enumZMode,
update=update_node_values_with_preset,
description="Changes Z calculation for different types of primitives",
)
cvg_x_alpha: bpy.props.BoolProperty(
update=update_node_values_with_preset,
description="Multiply coverage (amount primitive covers target pixel) with alpha and store result as coverage",
)
alpha_cvg_sel: bpy.props.BoolProperty(
update=update_node_values_with_preset,
description="Use coverage (amount primitive covers target pixel) as alpha instead of color combiner alpha",
)
force_bl: bpy.props.BoolProperty(
update=update_node_values_with_preset,
description="Always uses blending on. Default blending is conditionally only applied during partial coverage. Forcing blending will disable division step of the blender, so B input must be 1-A or there may be rendering issues. Always use this option when Z Buffering is off",
)
# cycle dependent - (P * A + M - B) / (A + B)
blend_p1: bpy.props.EnumProperty(
name="Color Source 1",
items=enumBlendColor,
update=update_node_values_with_preset,
)
blend_p2: bpy.props.EnumProperty(
name="Color Source 1",
items=enumBlendColor,
update=update_node_values_with_preset,
)
blend_m1: bpy.props.EnumProperty(
name="Color Source 2",
items=enumBlendColor,
update=update_node_values_with_preset,
)
blend_m2: bpy.props.EnumProperty(
name="Color Source 2",
items=enumBlendColor,
update=update_node_values_with_preset,
)
blend_a1: bpy.props.EnumProperty(
name="Alpha Source",
items=enumBlendAlpha,
update=update_node_values_with_preset,
)
blend_a2: bpy.props.EnumProperty(
name="Alpha Source",
items=enumBlendAlpha,
update=update_node_values_with_preset,
)
blend_b1: bpy.props.EnumProperty(
name="Alpha Mix",
items=enumBlendMix,
update=update_node_values_with_preset,
)
blend_b2: bpy.props.EnumProperty(
name="Alpha Mix",
items=enumBlendMix,
update=update_node_values_with_preset,
)
@property
def using_fog(self):
return self.g_fog or self.does_blender_use_input("G_BL_CLR_FOG") or self.does_blender_use_input("G_BL_A_FOG")
@property
def blend_color_inputs(self):
yield from (getattr(self, f"blend_{val}") for val in ("p1", "p2", "m1", "m2"))
@property
def blend_alpha_inputs(self):
yield from (getattr(self, f"blend_{val}") for val in ("a1", "a2", "b1", "b2"))
@property
def blend_inputs(self):
yield from self.blend_color_inputs
yield from self.blend_alpha_inputs
def has_prop_in_ucode(self, prop: str) -> bool:
return prop in geo_modes_in_ucode(bpy.context.scene.f3d_type).values()
def is_geo_mode_on(self, prop: str) -> bool:
return getattr(self, prop) if self.has_prop_in_ucode(prop) else False
def does_blender_use_input(self, setting: str) -> bool:
return any(input == setting for input in self.blend_inputs)
def attributes_to_dict(self, info: dict):
data = {}
for key, attr, default in info:
value = getattr(self, attr)
if value != default:
data[key] = value
return data
def attributes_from_dict(self, data: dict, info: dict):
for key, attr, default in info:
setattr(self, attr, data.get(key, default))
geo_mode_attributes = {**F3D_GEO_MODES, **F3DLX_GEO_MODES, **F3DEX3_GEO_MODES}
def geo_mode_to_dict(self):
data = {}
for key, attr in geo_modes_in_ucode(bpy.context.scene.f3d_type).items():
if getattr(self, attr):
data[key] = True
return data
def geo_mode_from_dict(self, data: dict):
for key, attr in self.geo_mode_attributes.items():
setattr(self, attr, data.get(key, False))
other_mode_h_attributes = [
("alphaDither", "g_mdsft_alpha_dither", "G_AD_DISABLE"),
("colorDither", "g_mdsft_rgb_dither", "G_CD_MAGICSQ"),
("chromaKey", "g_mdsft_combkey", "G_CK_NONE"),
("textureConvert", "g_mdsft_textconv", "G_TC_CONV"),
("textureFilter", "g_mdsft_text_filt", "G_TF_POINT"),
("lutFormat", "g_mdsft_textlut", "G_TT_NONE"),
("textureLoD", "g_mdsft_textlod", "G_TL_TILE"),
("textureDetail", "g_mdsft_textdetail", "G_TD_CLAMP"),
("perspectiveCorrection", "g_mdsft_textpersp", "G_TP_NONE"),
("cycleType", "g_mdsft_cycletype", "G_CYC_1CYCLE"),
("pipelineMode", "g_mdsft_pipeline", "G_PM_NPRIMITIVE"),
]
def other_mode_h_to_dict(self, lut_format=None):
data = self.attributes_to_dict(self.other_mode_h_attributes)
if lut_format:
data["lutFormat"] = lut_format
return data
def other_mode_h_from_dict(self, data: dict):
self.attributes_from_dict(data, self.other_mode_h_attributes)
other_mode_l_attributes = [
("alphaCompare", "g_mdsft_alpha_compare", "G_AC_NONE"),
("zSourceSelection", "g_mdsft_zsrcsel", "G_ZS_PIXEL"),
]
rendermode_flag_attributes = [
("aa", "aa_en", False),
("zTest", "z_cmp", False),
("zWrite", "z_upd", False),
("colorOnCvg", "clr_on_cvg", False),
("alphaOnCvg", "alpha_cvg_sel", False),
("mulCvgXAlpha", "cvg_x_alpha", False),
("forceBlend", "force_bl", False),
("readFB", "im_rd", False),
("cvgDst", "cvg_dst", "CVG_DST_CLAMP"),
("zMode", "zmode", "ZMODE_OPA"),
]
def other_mode_l_to_dict(self):
data = self.attributes_to_dict(self.other_mode_l_attributes)
if self.g_mdsft_zsrcsel == "G_ZS_PRIM":
data["primDepth"] = self.prim_depth.to_dict()
if self.set_rendermode:
two_cycle = self.g_mdsft_cycletype == "G_CYC_2CYCLE"
if self.rendermode_advanced_enabled:
blender_data = []
for i in range(2 if two_cycle else 1):
num = i + 1
color_attrs, alpha_attrs = (f"blend_p{num}", f"blend_m{num}"), (f"blend_a{num}", f"blend_b{num}")
blender_data.append(
{
"color": (getattr(self, color_attrs[0]), getattr(self, color_attrs[1])),
"alpha": (getattr(self, alpha_attrs[0]), getattr(self, alpha_attrs[1])),
}
)
data["renderMode"] = {
"flags": self.attributes_to_dict(self.rendermode_flag_attributes),
"blender": blender_data,
}
else:
data["renderMode"] = {
"presets": [self.rendermode_preset_cycle_1]
+ ([self.rendermode_preset_cycle_2] if two_cycle else [])
}
return data
def other_mode_l_from_dict(self, data: dict):
self.attributes_from_dict(data, self.other_mode_l_attributes)
self.prim_depth.from_dict(data.get("primDepth", {}))
render_mode = data.get("renderMode", {})
blender = render_mode.get("blender", [])
flags = render_mode.get("flags", {})
if render_mode:
self.set_rendermode = True
if not render_mode.get("presets", None) and (flags or blender):
self.rendermode_advanced_enabled = True
self.rendermode_preset_cycle_1, self.rendermode_preset_cycle_2 = render_mode.get(
"presets", [self.rendermode_preset_cycle_1, self.rendermode_preset_cycle_2]
)
self.attributes_from_dict(flags, self.rendermode_flag_attributes)
color_attrs = ("blend_p", "blend_m")
alpha_attrs = ("blend_a", "blend_b")
for i, cycle in enumerate(blender * 2 if len(blender) == 1 else blender):
num = str(i + 1)
color_attrs, alpha_attrs = (f"blend_p{num}", f"blend_m{num}"), (f"blend_a{num}", f"blend_b{num}")
self[color_attrs[0]], self[color_attrs[1]] = cycle.get(
"color", [self.get(color_attrs[0]), self.get(color_attrs[1])]
)
self[alpha_attrs[0]], self[alpha_attrs[1]] = cycle.get(
"alpha", [self.get(alpha_attrs[0]), self.get(alpha_attrs[1])]
)
def other_to_dict(self):
data = {}
if self.clip_ratio != 1.0:
data["clipRatio"] = self.clip_ratio
if self.g_mdsft_textlod == "G_TL_LOD" and self.num_textures_mipmapped != 1:
data["mipmapCount"] = self.num_textures_mipmapped
return data
def other_from_dict(self, data: dict):
self.clip_ratio = data.get("clipRatio", self.clip_ratio)
self.num_textures_mipmapped = data.get("mipmapCount", self.num_textures_mipmapped)
def to_dict(self):
data = {}
data["geometryMode"] = self.geo_mode_to_dict()
data["otherModeH"] = self.other_mode_h_to_dict()
data["otherModeL"] = self.other_mode_l_to_dict()
data["other"] = self.other_to_dict()
return data
def from_dict(self, data: dict):
self.geo_mode_from_dict(data.get("geometryMode", {}))
self.other_mode_h_from_dict(data.get("otherModeH", {}))
self.other_mode_l_from_dict(data.get("otherModeL", {}))
self.other_from_dict(data.get("other", {}))
def key(self):
return str(self.to_dict().items())
def draw_rdp_world_defaults(layout: UILayout, scene: Scene):
world = scene.world
if world is None:
layout.box().label(text="No World Selected In Scene", icon="WORLD")
return
rdp_defaults = world.rdp_defaults
col = layout.column()
col.box().label(text="RDP Default Settings", icon="WORLD")
multilineLabel(
col,
text="If a material setting is the same as the default setting\n"
"it won't be set, otherwise a revert will be added.",
)
if scene.gameEditorMode == "Homebrew":
multilineLabel(
col.box(),
text="Homebrew mode defaults to ucode defaults,\nmake sure to set your own.",
icon="INFO",
)
ui_geo_mode(rdp_defaults, world, col, True)
ui_upper_mode(rdp_defaults, world, col, True)
ui_lower_mode(rdp_defaults, world, col, True)
ui_other(rdp_defaults, world, col, True)
class DefaultRDPSettingsPanel(Panel):
bl_label = "RDP Default Settings"
bl_idname = "WORLD_PT_RDP_Default_Inspector"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "world"
bl_options = {"HIDE_HEADER"}
def draw(self, context):
draw_rdp_world_defaults(self.layout, context.scene)
class CelLevelProperty(PropertyGroup):
threshMode: bpy.props.EnumProperty(
items=enumCelThreshMode, name="Draw when", default="Lighter", update=update_cel_cutout_source
)
threshold: bpy.props.IntProperty(
name="Threshold",
description="Light level at which the boundary between cel levels occurs. One level is >= this value, the other is < it",
min=2,
max=255,
default=128,
)
tintType: bpy.props.EnumProperty(items=enumCelTintType, name="Tint type", default="Fixed")
tintFixedLevel: bpy.props.IntProperty(
name="Level",
description="0: original color <=> 255: fully tint color",
min=0,
max=255,
default=50,
)
tintFixedColor: bpy.props.FloatVectorProperty(
name="Tint color",
size=3,
min=0.0,
max=1.0,
subtype="COLOR",
)
tintSegmentNum: bpy.props.IntProperty(
name="Segment",
description="Segment number to store tint DL in",
min=8,
max=0xD,
default=8,
)
tintSegmentOffset: bpy.props.IntProperty(
name="Offset (instr)",
description="Number of instructions (8 bytes) within this DL to jump to",
min=0,
max=1000,
default=0,
)
tintLightSlot: bpy.props.IntProperty(
name="Light (/end)",
description="Which light to load RGB color from, counting from the end. 0 = ambient, 1 = last directional / point light, 2 = second-to-last, etc.",
min=0,
max=9,
default=1,
)
class CelShadingProperty(PropertyGroup):
tintPipeline: bpy.props.EnumProperty(items=enumCelTintPipeline, name="Tint pipeline", default="CC")
cutoutSource: bpy.props.EnumProperty(
items=enumCelCutoutSource,
name="Cutout",
default="ENVIRONMENT",
update=update_cel_cutout_source,
)
levels: bpy.props.CollectionProperty(type=CelLevelProperty, name="Cel levels")
def celGetMaterialLevels(materialName):
material = bpy.data.materials.get(materialName)
if material is None:
raise PluginError(f"Could not find material {materialName}")
return material.f3d_mat.cel_shading.levels
class CelLevelAdd(bpy.types.Operator):
bl_idname = "material.f3d_cel_level_add"
bl_label = "Add Cel Level"
bl_options = {"REGISTER", "UNDO"}
materialName: bpy.props.StringProperty()
def execute(self, context):
levels = celGetMaterialLevels(self.materialName)
levels.add()
return {"FINISHED"}
class CelLevelRemove(bpy.types.Operator):
bl_idname = "material.f3d_cel_level_remove"
bl_label = "Remove Last Level"
bl_options = {"REGISTER", "UNDO"}
materialName: bpy.props.StringProperty()
def execute(self, context):
levels = celGetMaterialLevels(self.materialName)
levels.remove(len(levels) - 1)
return {"FINISHED"}
class ApplyMaterialPresetOperator(Operator):
bl_idname = "material.f3d_preset_apply"
bl_label = "Apply F3D Material Preset"
filepath: bpy.props.StringProperty()
def execute(self, context: Context):
material_apply_preset(context.material, self.filepath)
return {"FINISHED"}
# modules/bpy_types.py -> Menu
class MATERIAL_MT_f3d_presets(Menu):
bl_label = "F3D Material Presets"
preset_operator = ApplyMaterialPresetOperator.bl_idname
def draw(self, _context):
"""
Define these on the subclass:
- preset_operator (string)
- preset_subdir (string)
Optionally:
- preset_add_operator (string)
- preset_extensions (set of strings)
- preset_operator_defaults (dict of keyword args)
"""
ext_valid = getattr(self, "preset_extensions", {".py", ".xml"})
props_default = getattr(self, "preset_operator_defaults", None)
add_operator = getattr(self, "preset_add_operator", None)
game = bpy.context.scene.gameEditorMode.lower()
paths = bpy.utils.preset_paths("f3d/user")
if not bpy.context.scene.f3dUserPresetsOnly:
if game == "sm64":
if bpy.context.scene.fast64.sm64.lighting_engine_presets:
paths += bpy.utils.preset_paths("f3d/sm64_lighting_engine")
else:
paths += bpy.utils.preset_paths("f3d/sm64")
elif game == "oot":
paths += bpy.utils.preset_paths("f3d/oot")
if bpy.context.scene.f3d_type == "F3DEX3":
paths += bpy.utils.preset_paths("f3d/oot_f3dex3")
self.path_menu(
paths,
self.preset_operator,
props_default=props_default,
filter_ext=lambda ext: ext.lower() in ext_valid,
add_operator=add_operator,
)
class AddPresetF3D(AddPresetBase, Operator):
"""Add an F3D Material Preset"""
bl_idname = "material.f3d_preset_add"
bl_label = "Add F3D Material Preset"
preset_menu = "MATERIAL_MT_f3d_presets"
# variable used for all preset values
# do NOT set "mat" in this operator, even in a for loop! it overrides this value
preset_defines = ["f3d_mat = bpy.context.material.f3d_mat"]
# properties to store in the preset
preset_values = [
"f3d_mat",
]
# where to store the preset
preset_subdir = "f3d/user"
defaults = [
"Custom",
# "Shaded Texture",
]
ignore_props = {
"f3d_mat.tex0.tex",
"f3d_mat.tex0.tex_format",
"f3d_mat.tex0.ci_format",
"f3d_mat.tex0.use_tex_reference",
"f3d_mat.tex0.tex_reference",
"f3d_mat.tex0.tex_reference_size",
"f3d_mat.tex0.pal_reference",
"f3d_mat.tex0.pal_reference_size",
"f3d_mat.tex0.S",
"f3d_mat.tex0.T",
"f3d_mat.tex0.menu",
"f3d_mat.tex0.autoprop",
"f3d_mat.tex0.save_large_texture",
"f3d_mat.tex0.tile_scroll",
"f3d_mat.tex0.tile_scroll.s",
"f3d_mat.tex0.tile_scroll.t",
"f3d_mat.tex0.tile_scroll.interval",
"f3d_mat.tex1.tex",
"f3d_mat.tex1.tex_format",
"f3d_mat.tex1.ci_format",
"f3d_mat.tex1.use_tex_reference",
"f3d_mat.tex1.tex_reference",
"f3d_mat.tex1.tex_reference_size",
"f3d_mat.tex1.pal_reference",
"f3d_mat.tex1.pal_reference_size",
"f3d_mat.tex1.S",
"f3d_mat.tex1.T",
"f3d_mat.tex1.menu",
"f3d_mat.tex1.autoprop",
"f3d_mat.tex1.save_large_texture",
"f3d_mat.tex1.tile_scroll",
"f3d_mat.tex1.tile_scroll.s",
"f3d_mat.tex1.tile_scroll.t",
"f3d_mat.tex1.tile_scroll.interval",
"f3d_mat.tex_scale",
"f3d_mat.scale_autoprop",
"f3d_mat.uv_basis",
"f3d_mat.UVanim0",
"f3d_mat.UVanim1",
"f3d_mat.menu_procAnim",
"f3d_mat.menu_geo",
"f3d_mat.menu_upper",
"f3d_mat.menu_lower",
"f3d_mat.menu_other",
"f3d_mat.menu_lower_render",
"f3d_mat.f3d_update_flag",
"f3d_mat.name",
"f3d_mat.use_large_textures",
}
def execute(self, context):
import os
from bpy.utils import is_path_builtin
if hasattr(self, "pre_cb"):
self.pre_cb(context)
preset_menu_class = getattr(bpy.types, self.preset_menu)
is_xml = getattr(preset_menu_class, "preset_type", None) == "XML"
is_preset_add = not (self.remove_name or self.remove_active)
if is_xml:
ext = ".xml"
else:
ext = ".py"
name = self.name.strip() if is_preset_add else self.name
if is_preset_add:
if not name:
return {"FINISHED"}
filename = self.as_filename(name)
if filename in material_presets or filename == "custom":
self.report({"WARNING"}, "Unable to delete/overwrite default presets.")
return {"CANCELLED"}
# Reset preset name
wm = bpy.data.window_managers[0]
if name == wm.preset_name:
wm.preset_name = "New Preset"
filename = self.as_filename(name)
context.material.f3d_mat.presetName = bpy.path.display_name(filename)
target_path = os.path.join("presets", self.preset_subdir)
try:
target_path = bpy.utils.user_resource("SCRIPTS", target_path, create=True)
except: # 3.0
target_path = bpy.utils.user_resource("SCRIPTS", path=target_path, create=True)
if not target_path:
self.report({"WARNING"}, "Failed to create presets path")
return {"CANCELLED"}
filepath = os.path.join(target_path, filename) + ext
if hasattr(self, "add"):
self.add(context, filepath)
else:
logger.info("Writing Preset: %r" % filepath)
if is_xml:
import rna_xml
rna_xml.xml_file_write(context, filepath, preset_menu_class.preset_xml_map)
else:
def rna_recursive_attr_expand(value, rna_path_step, level):
if rna_path_step in self.ignore_props:
return
if isinstance(value, PropertyGroup):
for sub_value_attr in value.bl_rna.properties.keys():
if sub_value_attr == "rna_type":
continue
sub_value = getattr(value, sub_value_attr)
rna_recursive_attr_expand(sub_value, "%s.%s" % (rna_path_step, sub_value_attr), level)
elif type(value).__name__ == "bpy_prop_collection_idprop": # could use nicer method
file_preset.write("%s.clear()\n" % rna_path_step)
for sub_value in value:
file_preset.write("item_sub_%d = %s.add()\n" % (level, rna_path_step))
rna_recursive_attr_expand(sub_value, "item_sub_%d" % level, level + 1)
else:
# convert thin wrapped sequences
# to simple lists to repr()
try:
value = value[:]
except:
pass
file_preset.write("%s = %r\n" % (rna_path_step, value))
file_preset = open(filepath, "w", encoding="utf-8")
file_preset.write("import bpy\n")
if hasattr(self, "preset_defines"):
for rna_path in self.preset_defines:
exec(rna_path)
file_preset.write("%s\n" % rna_path)
file_preset.write("\n")
file_preset.write("bpy.context.material.f3d_update_flag = True\n")
for rna_path in self.preset_values:
value = eval(rna_path)
rna_recursive_attr_expand(value, rna_path, 1)
file_preset.write("bpy.context.material.f3d_update_flag = False\n")
file_preset.write(
"f3d_mat.use_default_lighting = f3d_mat.use_default_lighting # Force nodes update\n"
)
file_preset.close()
presetName = bpy.path.display_name(filename)
preset_menu_class.bl_label = presetName
for otherMat in bpy.data.materials:
if otherMat.f3d_mat.presetName == presetName and otherMat != context.material:
update_preset_manual_v4(otherMat, filename)
context.material.f3d_mat.presetName = bpy.path.display_name(filename)
else:
if self.remove_active:
name = preset_menu_class.bl_label
filename = self.as_filename(name)
presetName = bpy.path.display_name(filename)
if filename in material_presets or filename == "custom":
self.report({"WARNING"}, "Unable to delete/overwrite default presets.")
return {"CANCELLED"}
# fairly sloppy but convenient.
filepath = bpy.utils.preset_find(name, self.preset_subdir, ext=ext)
if not filepath:
filepath = bpy.utils.preset_find(name, self.preset_subdir, display_name=True, ext=ext)
if not filepath:
return {"CANCELLED"}
# Do not remove bundled presets
if is_path_builtin(filepath):
self.report({"WARNING"}, "Unable to remove default presets")
return {"CANCELLED"}
try:
if hasattr(self, "remove"):
self.remove(context, filepath)
else:
os.remove(filepath)
except Exception as e:
self.report({"ERROR"}, "Unable to remove preset: %r" % e)
import traceback
traceback.print_exc()
return {"CANCELLED"}
# XXX, stupid!
preset_menu_class.bl_label = "Presets"
for material in bpy.data.materials:
if material.f3d_mat.presetName == presetName:
material.f3d_mat.presetName = "Custom"
if hasattr(self, "post_cb"):
self.post_cb(context)
return {"FINISHED"}
def convertToNewMat(material):
old_to_new_props = {
"presetName": "presetName",
"scale_autoprop": "scale_autoprop",
"uv_basis": "uv_basis",
"combiner1": "combiner1",
"combiner2": "combiner2",
"menu_procAnim": "menu_procAnim",
"UVanim0": "UVanim",
"UVanim1": "UVanim_tex1",
"tex_scale": "tex_scale",
"tex0": "tex0",
"tex1": "tex1",
"set_prim": "set_prim",
"set_lights": "set_lights",
"set_env": "set_env",
"set_blend": "set_blend",
"set_key": "set_key",
"set_k0_5": "set_k0_5",
"set_combiner": "set_combiner",
"use_default_lighting": "use_default_lighting",
"blend_color": "blend_color",
"key_scale": "key_scale",
"key_width": "key_width",
"k0": "k0",
"k1": "k1",
"k2": "k2",
"k3": "k3",
"k4": "k4",
"k5": "k5",
"prim_lod_frac": "prim_lod_frac",
"prim_lod_min": "prim_lod_min",
"default_light_color": "default_light_color",
"ambient_light_color": "ambient_light_color",
"fog_color": "fog_color",
"fog_position": "fog_position",
"set_fog": "set_fog",
"use_global_fog": "use_global_fog",
"menu_geo": "menu_geo",
"menu_upper": "menu_upper",
"menu_lower": "menu_lower",
"menu_other": "menu_other",
"menu_lower_render": "menu_lower_render",
"rdp_settings": "rdp_settings",
}
for new, old in old_to_new_props.items():
upgrade_old_prop(material.f3d_mat, new, material, old)
# Colors
nodes = material.node_tree.nodes
if material.mat_ver == 3:
prim = nodes["Primitive Color Output"].inputs[0].default_value
env = nodes["Environment Color Output"].inputs[0].default_value
else:
prim = nodes["Primitive Color"].outputs[0].default_value
env = nodes["Environment Color"].outputs[0].default_value
material.f3d_mat.prim_color = prim
material.f3d_mat.env_color = env
if "Chroma Key Center" in nodes:
material.f3d_mat.key_center = nodes["Chroma Key Center"].outputs[0].default_value
# lights
for i in range(1, 8):
light_attr = f"f3d_light{str(i)}"
upgrade_old_prop(material.f3d_mat, light_attr, material, light_attr)
class F3DMaterialProperty(PropertyGroup):
presetName: bpy.props.StringProperty(
name="Preset Name",
default="Custom",
)
scale_autoprop: bpy.props.BoolProperty(
name="Auto Set Scale",
default=True,
update=update_tex_values,
)
uv_basis: bpy.props.EnumProperty(
name="UV Basis",
default="TEXEL0",
items=enumTexUV,
update=update_tex_values,
)
# Combiners
combiner1: bpy.props.PointerProperty(type=CombinerProperty)
combiner2: bpy.props.PointerProperty(type=CombinerProperty)
# Texture animation
menu_procAnim: bpy.props.BoolProperty()
UVanim0: bpy.props.PointerProperty(type=ProcAnimVectorProperty)
UVanim1: bpy.props.PointerProperty(type=ProcAnimVectorProperty)
# material textures
tex_scale: bpy.props.FloatVectorProperty(
min=0,
max=1,
size=2,
default=(1, 1),
step=1,
update=update_tex_values,
)
tex0: bpy.props.PointerProperty(type=TextureProperty, name="tex0")
tex1: bpy.props.PointerProperty(type=TextureProperty, name="tex1")
# Should Set?
set_prim: bpy.props.BoolProperty(
default=True,
update=update_node_values_with_preset,
)
set_lights: bpy.props.BoolProperty(
default=True,
update=update_node_values_with_preset,
)
set_env: bpy.props.BoolProperty(
default=False,
update=update_node_values_with_preset,
)
set_blend: bpy.props.BoolProperty(
default=False,
update=update_node_values_with_preset,
)
set_key: bpy.props.BoolProperty(
default=True,
update=update_node_values_with_preset,
)
set_k0_5: bpy.props.BoolProperty(
default=True,
update=update_node_values_with_preset,
)
set_combiner: bpy.props.BoolProperty(
default=True,
update=update_node_values_with_preset,
)
use_default_lighting: bpy.props.BoolProperty(
default=True,
update=update_node_values_without_preset,
)
# Blend Color
blend_color: bpy.props.FloatVectorProperty(
name="Blend Color",
subtype="COLOR",
size=4,
min=0,
max=1,
default=(0, 0, 0, 1),
)
prim_color: bpy.props.FloatVectorProperty(
name="Primitive Color",
subtype="COLOR",
size=4,
min=0,
max=1,
default=(1, 1, 1, 1),
update=get_color_input_update_callback("prim_color", "Prim"),
)
env_color: bpy.props.FloatVectorProperty(
name="Environment Color",
subtype="COLOR",
size=4,
min=0,
max=1,
default=(1, 1, 1, 1),
update=get_color_input_update_callback("env_color", "Env"),
)
key_center: bpy.props.FloatVectorProperty(
name="Key Center",
subtype="COLOR",
size=4,
min=0,
max=1,
default=(1, 1, 1, 1),
update=update_node_values_without_preset,
)
# Chroma
key_scale: bpy.props.FloatVectorProperty(
name="Key Scale",
min=0,
max=1,
step=1,
update=update_node_values_with_preset,
)
key_width: bpy.props.FloatVectorProperty(
name="Key Width",
min=0,
max=16,
update=update_node_values_with_preset,
)
# Convert
k0: bpy.props.FloatProperty(
min=-1,
max=1,
default=175 / 255,
step=1,
update=update_node_values_with_preset,
)
k1: bpy.props.FloatProperty(
min=-1,
max=1,
default=-43 / 255,
step=1,
update=update_node_values_with_preset,
)
k2: bpy.props.FloatProperty(
min=-1,
max=1,
default=-89 / 255,
step=1,
update=update_node_values_with_preset,
)
k3: bpy.props.FloatProperty(
min=-1,
max=1,
default=222 / 255,
step=1,
update=update_node_values_with_preset,
)
k4: bpy.props.FloatProperty(
min=-1,
max=1,
default=114 / 255,
step=1,
update=update_node_values_with_preset,
)
k5: bpy.props.FloatProperty(
min=-1,
max=1,
default=42 / 255,
step=1,
update=update_node_values_with_preset,
)
# Prim
prim_lod_frac: bpy.props.FloatProperty(
name="Prim LOD Frac",
min=0,
max=1,
step=1,
update=update_node_values_with_preset,
)
prim_lod_min: bpy.props.FloatProperty(
name="Min LOD Ratio",
min=0,
max=1,
step=1,
update=update_node_values_with_preset,
)
# lights
default_light_color: bpy.props.FloatVectorProperty(
name="Default Light Color",
subtype="COLOR",
size=4,
min=0,
max=1,
default=(1, 1, 1, 1),
update=update_light_properties,
)
set_ambient_from_light: bpy.props.BoolProperty(
"Automatic Ambient Color", default=True, update=update_light_properties
)
ambient_light_color: bpy.props.FloatVectorProperty(
name="Ambient Light Color",
subtype="COLOR",
size=4,
min=0,
max=1,
default=(0.5, 0.5, 0.5, 1),
update=update_light_properties,
)
f3d_light1: bpy.props.PointerProperty(type=Light, update=F3DOrganizeLights)
f3d_light2: bpy.props.PointerProperty(type=Light, update=F3DOrganizeLights)
f3d_light3: bpy.props.PointerProperty(type=Light, update=F3DOrganizeLights)
f3d_light4: bpy.props.PointerProperty(type=Light, update=F3DOrganizeLights)
f3d_light5: bpy.props.PointerProperty(type=Light, update=F3DOrganizeLights)
f3d_light6: bpy.props.PointerProperty(type=Light, update=F3DOrganizeLights)
f3d_light7: bpy.props.PointerProperty(type=Light, update=F3DOrganizeLights)
# Ambient Occlusion
ao_ambient: bpy.props.FloatProperty(
name="AO Ambient",
min=0.0,
max=1.0,
default=1.0,
description="How much ambient occlusion (vertex alpha) affects ambient light intensity",
update=update_node_values_without_preset,
)
ao_directional: bpy.props.FloatProperty(
name="AO Directional",
min=0.0,
max=1.0,
default=0.625,
description="How much ambient occlusion (vertex alpha) affects directional light intensity",
update=update_node_values_without_preset,
)
ao_point: bpy.props.FloatProperty(
name="AO Point",
min=0.0,
max=1.0,
default=0.0,
description="How much ambient occlusion (vertex alpha) affects point light intensity",
update=update_node_values_without_preset,
)
set_ao: bpy.props.BoolProperty(update=update_node_values_without_preset)
# Fresnel
fresnel_lo: bpy.props.FloatProperty(
name="Fresnel lo",
min=-1000.0,
max=1000.0,
default=0.7,
description="Dot product value which gives shade alpha = 0. The dot product ranges from 1 when the normal points directly at the camera, to 0 when it points sideways",
update=update_node_values_without_preset,
)
fresnel_hi: bpy.props.FloatProperty(
name="Fresnel hi",
min=-1000.0,
max=1000.0,
default=0.4,
description="Dot product value which gives shade alpha = FF. The dot product ranges from 1 when the normal points directly at the camera, to 0 when it points sideways",
update=update_node_values_without_preset,
)
set_fresnel: bpy.props.BoolProperty(update=update_node_values_without_preset)
# Attribute Offsets
attroffs_st: bpy.props.FloatVectorProperty(
name="ST Attr Offset",
size=2,
min=-1024.0,
max=1024.0,
default=(0.0, 0.0),
description="Offset applied to ST (UV) coordinates, after texture scale. Units are texels. Usually for UV scrolling",
update=update_node_values_without_preset,
)
attroffs_z: bpy.props.IntProperty(
name="Z Attr Offset",
min=-0x8000,
max=0x7FFF,
default=-2,
description="Offset applied to Z coordinate. To fix decals, set Z mode to opaque and set Z attr offset to something like -2",
update=update_node_values_without_preset,
)
set_attroffs_st: bpy.props.BoolProperty(update=update_node_values_without_preset)
set_attroffs_z: bpy.props.BoolProperty(update=update_node_values_without_preset)
# Fog Properties
fog_color: bpy.props.FloatVectorProperty(
name="Fog Color",
subtype="COLOR",
size=4,
min=0,
max=1,
default=(0, 0, 0, 1),
update=update_node_values_without_preset,
)
# TODO: (V5) dragorn421 should ask me if this is _actually_ the fog position max because this seems wrong to him
fog_position: bpy.props.IntVectorProperty(
name="Fog Range",
size=2,
min=0,
max=0x10000,
default=(985, 1000),
update=update_node_values_without_preset,
)
set_fog: bpy.props.BoolProperty(update=update_node_values_without_preset)
use_global_fog: bpy.props.BoolProperty(default=False, update=update_node_values_without_preset)
# geometry mode
menu_geo: bpy.props.BoolProperty()
menu_upper: bpy.props.BoolProperty()
menu_lower: bpy.props.BoolProperty()
menu_other: bpy.props.BoolProperty()
menu_lower_render: bpy.props.BoolProperty()
rdp_settings: bpy.props.PointerProperty(type=RDPSettings)
draw_layer: bpy.props.PointerProperty(type=DrawLayerProperty)
use_large_textures: bpy.props.BoolProperty(name="Large Texture Mode")
large_edges: bpy.props.EnumProperty(items=enumLargeEdges, default="Clamp")
expand_cel_shading_ui: bpy.props.BoolProperty(name="Expand Cel Shading UI")
use_cel_shading: bpy.props.BoolProperty(name="Use Cel Shading", update=update_cel_cutout_source)
cel_shading: bpy.props.PointerProperty(type=CelShadingProperty)
def key(self) -> F3DMaterialHash:
useDefaultLighting = self.set_lights and self.use_default_lighting
return (
self.scale_autoprop,
self.uv_basis,
self.UVanim0.key(),
self.UVanim1.key(),
tuple([round(value, 4) for value in self.tex_scale]),
self.tex0.key(),
self.tex1.key(),
self.rdp_settings.key(),
self.draw_layer.key(),
self.use_large_textures,
self.use_cel_shading,
self.cel_shading.tintPipeline if self.use_cel_shading else None,
(
tuple(
[
(
c.threshMode,
c.threshold,
c.tintType,
c.tintFixedLevel,
c.tintFixedColor,
c.tintSegmentNum,
c.tintSegmentOffset,
c.tintLightSlot,
)
for c in self.cel_shading.levels
]
)
if self.use_cel_shading
else None
),
self.use_default_lighting,
self.set_blend,
self.set_prim,
self.set_env,
self.set_key,
self.set_k0_5,
self.set_combiner,
self.set_lights,
self.set_fog,
tuple([round(value, 4) for value in self.blend_color]) if self.set_blend else None,
tuple([round(value, 4) for value in self.prim_color]) if self.set_prim else None,
round(self.prim_lod_frac, 4) if self.set_prim else None,
round(self.prim_lod_min, 4) if self.set_prim else None,
tuple([round(value, 4) for value in self.env_color]) if self.set_env else None,
tuple([round(value, 4) for value in self.key_center]) if self.set_key else None,
tuple([round(value, 4) for value in self.key_scale]) if self.set_key else None,
tuple([round(value, 4) for value in self.key_width]) if self.set_key else None,
round(self.k0, 4) if self.set_k0_5 else None,
round(self.k1, 4) if self.set_k0_5 else None,
round(self.k2, 4) if self.set_k0_5 else None,
round(self.k3, 4) if self.set_k0_5 else None,
round(self.k4, 4) if self.set_k0_5 else None,
round(self.k5, 4) if self.set_k0_5 else None,
self.combiner1.key() if self.set_combiner else None,
self.combiner2.key() if self.set_combiner else None,
(
tuple([round(value, 4) for value in (self.ao_ambient, self.ao_directional, self.ao_point)])
if self.set_ao
else None
),
tuple([round(value, 4) for value in (self.fresnel_lo, self.fresnel_hi)]) if self.set_fresnel else None,
tuple([round(value, 4) for value in self.attroffs_st]) if self.set_attroffs_st else None,
self.attroffs_z if self.set_attroffs_z else None,
tuple([round(value, 4) for value in self.fog_color]) if self.set_fog else None,
tuple([round(value, 4) for value in self.fog_position]) if self.set_fog else None,
tuple([round(value, 4) for value in self.default_light_color]) if useDefaultLighting else None,
self.set_ambient_from_light if useDefaultLighting else None,
(
tuple([round(value, 4) for value in self.ambient_light_color])
if useDefaultLighting and not self.set_ambient_from_light
else None
),
self.f3d_light1 if not useDefaultLighting else None,
self.f3d_light2 if not useDefaultLighting else None,
self.f3d_light3 if not useDefaultLighting else None,
self.f3d_light4 if not useDefaultLighting else None,
self.f3d_light5 if not useDefaultLighting else None,
self.f3d_light6 if not useDefaultLighting else None,
self.f3d_light7 if not useDefaultLighting else None,
)
class UnlinkF3DImage0(Operator):
bl_idname = "image.tex0_unlink"
bl_label = "Unlink F3D Image"
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):
context.material.f3d_mat.tex0.tex = None
return {"FINISHED"} # must return a set
class UnlinkF3DImage1(Operator):
bl_idname = "image.tex1_unlink"
bl_label = "Unlink F3D Image"
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):
context.material.f3d_mat.tex1.tex = None
return {"FINISHED"} # must return a set
class UpdateF3DNodes(Operator):
bl_idname = "material.update_f3d_nodes"
bl_label = "Update F3D Nodes"
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):
if context is None or not hasattr(context, "material") or context.material is None:
self.report({"ERROR"}, "Material not found in context.")
return {"CANCELLED"}
if not context.material.is_f3d:
self.report({"ERROR"}, "Material is not F3D.")
return {"CANCELLED"}
material = context.material
material.f3d_update_flag = True
try:
update_node_values_of_material(material, context)
material.f3d_mat.presetName = "Custom"
except Exception as exc:
material.f3d_update_flag = False
raise exc
material.f3d_update_flag = False
return {"FINISHED"} # must return a set
class F3DRenderSettingsPanel(Panel):
bl_label = "F3D Render Settings"
bl_idname = "OBJECT_PT_F3D_RENDER_SETTINGS_PANEL"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
layout.ui_units_x = 16
renderSettings = context.scene.fast64.renderSettings
isF3DEX3 = bpy.context.scene.f3d_type == "F3DEX3"
globalSettingsBox = layout.box()
labelbox = globalSettingsBox.box()
labelbox.label(text="Global Settings")
labelbox.ui_units_x = 6
# Only show the update preview UI if the render engine is EEVEE,
# as there's no point in updating the nodes otherwise.
if context.scene.render.engine in {
"BLENDER_EEVEE", # <4.2
"BLENDER_EEVEE_NEXT", # 4.2+
}:
updatePreviewRow = globalSettingsBox.row()
updatePreviewRow.prop(renderSettings, "enableAutoUpdatePreview")
if not renderSettings.enableAutoUpdatePreview:
updatePreviewRow.operator(ManualUpdatePreviewOperator.bl_idname)
globalSettingsBox.separator()
globalSettingsBox.prop(renderSettings, "enableFogPreview")
prop_split(globalSettingsBox, renderSettings, "fogPreviewColor", "Fog Color")
prop_split(globalSettingsBox, renderSettings, "fogPreviewPosition", "Fog Position")
prop_split(globalSettingsBox, renderSettings, "clippingPlanes", "Clipping Planes")
globalSettingsBox.separator(factor=0.125)
# TODO: (v5) add headings
prop_split(globalSettingsBox, renderSettings, "ambientColor", "Ambient Light")
prop_split(globalSettingsBox, renderSettings, "light0Color", "Light 0 Color")
prop_split(globalSettingsBox, renderSettings, "light0Direction", "Light 0 Direction")
if isF3DEX3:
prop_split(globalSettingsBox, renderSettings, "light0SpecSize", "Light 0 Specular Size")
prop_split(globalSettingsBox, renderSettings, "light1Color", "Light 1 Color")
prop_split(globalSettingsBox, renderSettings, "light1Direction", "Light 1 Direction")
if isF3DEX3:
prop_split(globalSettingsBox, renderSettings, "light1SpecSize", "Light 1 Specular Size")
prop_split(globalSettingsBox, renderSettings, "useWorldSpaceLighting", "Use World Space Lighting")
if context.scene.gameEditorMode in ["SM64", "OOT"]:
layout.separator(factor=0.5)
gameSettingsBox = layout.box()
gameSettingsBox.label(text="Preview Context")
match context.scene.gameEditorMode:
case "SM64":
if renderSettings.sm64Area is not None:
gameSettingsBox.prop(renderSettings, "useObjectRenderPreview", text="Use Area for Preview")
gameSettingsBox.prop(renderSettings, "sm64Area")
case "OOT":
if renderSettings.ootSceneObject is not None:
gameSettingsBox.prop(renderSettings, "useObjectRenderPreview", text="Use Scene for Preview")
gameSettingsBox.prop(renderSettings, "ootSceneObject")
if renderSettings.ootSceneObject is not None:
b = gameSettingsBox.column()
r = b.row().split(factor=0.4)
r.prop(renderSettings, "ootSceneHeader")
header = ootGetSceneOrRoomHeader(
renderSettings.ootSceneObject,
renderSettings.ootSceneHeader,
False,
)
if header is None:
r.label(text="Header does not exist.", icon="QUESTION")
else:
numLightsNeeded = 1
if header.skyboxLighting == "Custom":
r2 = b.row().split(factor=0.4)
r2.prop(renderSettings, "ootForceTimeOfDay")
if renderSettings.ootForceTimeOfDay:
r2.label(text="Light Index sets first of four lights.", icon="INFO")
numLightsNeeded = 4
if header.skyboxLighting != "LIGHT_MODE_TIME":
r.prop(renderSettings, "ootLightIdx")
if renderSettings.ootLightIdx + numLightsNeeded > len(header.lightList):
b.label(text="Light does not exist.", icon="QUESTION")
if header.skyboxLighting == "LIGHT_MODE_TIME" or (
header.skyboxLighting == "Custom" and renderSettings.ootForceTimeOfDay
):
r.prop(renderSettings, "ootTime")
case _:
pass
def draw_f3d_render_settings(self, context):
layout: UILayout = self.layout
layout.popover(F3DRenderSettingsPanel.bl_idname)
mat_classes = (
UpdateColorManagementPopup,
UnlinkF3DImage0,
UnlinkF3DImage1,
DrawLayerProperty,
ApplyMaterialPresetOperator,
MATERIAL_MT_f3d_presets,
AddPresetF3D,
F3DPanel,
CreateFast3DMaterial,
RecreateF3DNodes,
TextureFieldProperty,
SetTileSizeScrollProperty,
TextureProperty,
CombinerProperty,
ProceduralAnimProperty,
ProcAnimVectorProperty,
PrimDepthSettings,
RDPSettings,
DefaultRDPSettingsPanel,
CelLevelProperty,
CelShadingProperty,
CelLevelAdd,
CelLevelRemove,
F3DMaterialProperty,
ReloadDefaultF3DPresets,
UpdateF3DNodes,
F3DRenderSettingsPanel,
F3DMeshPanel,
)
def findF3DPresetPath(filename):
try:
presetPath = bpy.utils.user_resource("SCRIPTS", os.path.join("presets", "f3d"), create=True)
except: # 3.0
presetPath = bpy.utils.user_resource("SCRIPTS", path=os.path.join("presets", "f3d"), create=True)
for subdir in os.listdir(presetPath):
subPath = os.path.join(presetPath, subdir)
if os.path.isdir(subPath):
for preset in os.listdir(subPath):
if preset[:-3] == filename:
return os.path.join(subPath, filename) + ".py"
raise PluginError("Preset " + str(filename) + " not found.")
def getF3DPresetPath(filename, subdir):
try:
presetPath = bpy.utils.user_resource("SCRIPTS", os.path.join("presets", subdir), create=True)
except: # 3.0
presetPath = bpy.utils.user_resource("SCRIPTS", path=os.path.join("presets", subdir), create=True)
return os.path.join(presetPath, filename) + ".py"
def savePresets():
for subdir, presets in material_presets.items():
for filename, preset in presets.items():
filepath = getF3DPresetPath(filename, "f3d/" + subdir)
file_preset = open(filepath, "w", encoding="utf-8")
file_preset.write(preset)
file_preset.close()
def mat_register():
for cls in mat_classes:
register_class(cls)
savePresets()
Scene.f3d_type = bpy.props.EnumProperty(
name="Microcode", items=enumF3D, default="F3D", update=update_all_material_nodes
)
Scene.packed_normals_algorithm = bpy.props.EnumProperty(name="Packed normals alg", items=enumPackedNormalsAlgorithm)
# RDP Defaults
World.rdp_defaults = bpy.props.PointerProperty(type=RDPSettings)
World.menu_geo = bpy.props.BoolProperty()
World.menu_upper = bpy.props.BoolProperty()
World.menu_lower = bpy.props.BoolProperty()
World.menu_other = bpy.props.BoolProperty()
World.menu_layers = bpy.props.BoolProperty()
Material.is_f3d = bpy.props.BoolProperty()
Material.mat_ver = bpy.props.IntProperty(default=1)
Material.f3d_update_flag = bpy.props.BoolProperty()
Material.f3d_mat = bpy.props.PointerProperty(type=F3DMaterialProperty)
Material.menu_tab = bpy.props.EnumProperty(items=menu_items_enum)
Scene.f3dUserPresetsOnly = bpy.props.BoolProperty(name="User Presets Only")
Scene.f3d_simple = bpy.props.BoolProperty(name="Display Simple", default=True)
Object.use_f3d_culling = bpy.props.BoolProperty(
name="Use Culling", description="F3DEX: Adds culling vertices", default=True
)
Object.ignore_render = bpy.props.BoolProperty(name="Ignore Render")
Object.ignore_collision = bpy.props.BoolProperty(name="Ignore Collision")
Object.bleed_independently = bpy.props.BoolProperty(
name="Bleed Independently",
description="While bleeding, this object will not inherit properties from previously drawn meshes in the drawing graph",
)
Object.f3d_lod_z = bpy.props.IntProperty(
name="F3D LOD Z",
min=1,
default=10,
)
Object.f3d_lod_always_render_farthest = bpy.props.BoolProperty(name="Always Render Farthest LOD")
Object.is_occlusion_planes = bpy.props.BoolProperty(name="Is Occlusion Planes")
VIEW3D_HT_header.append(draw_f3d_render_settings)
bpy.app.handlers.load_post.append(load_handler)
def mat_unregister():
VIEW3D_HT_header.remove(draw_f3d_render_settings)
while load_handler in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(load_handler)
del Material.menu_tab
del Material.f3d_mat
del Material.is_f3d
del Material.mat_ver
del Material.f3d_update_flag
del Scene.f3d_simple
del Object.ignore_render
del Object.ignore_collision
del Object.bleed_independently
del Object.use_f3d_culling
del Scene.f3dUserPresetsOnly
del Object.f3d_lod_z
del Object.f3d_lod_always_render_farthest
del Object.is_occlusion_planes
for cls in reversed(mat_classes):
unregister_class(cls)
# WARNING: Adding new presets will break any custom presets added afterward.
enumMaterialPresets = [
("Custom", "Custom", "Custom"),
("Unlit Texture", "Unlit Texture", "Unlit Texture"),
("Unlit Texture Cutout", "Unlit Texture Cutout", "Unlit Texture Cutout"),
("Shaded Solid", "Shaded Solid", "Shaded Solid"),
("Decal On Shaded Solid", "Decal On Shaded Solid", "Decal On Shaded Solid"),
("Shaded Texture", "Shaded Texture", "Shaded Texture"),
("Shaded Texture Cutout", "Shaded Texture Cutout", "Shaded Texture Cutout"),
(
"Shaded Texture Transparent",
"Shaded Texture Transparent (Prim Alpha)",
"Shaded Texture Transparent (Prim Alpha)",
),
("Vertex Colored Texture", "Vertex Colored Texture", "Vertex Colored Texture"),
("Environment Mapped", "Environment Mapped", "Environment Mapped"),
("Fog Shaded Texture", "Fog Shaded Texture", "Fog Shaded Texture"),
("Fog Shaded Texture Cutout", "Fog Shaded Texture Cutout", "Fog Shaded Texture Cutout"),
(
"Fog Shaded Texture Transparent",
"Fog Shaded Texture Transparent (Prim Alpha)",
"Fog Shaded Texture Transparent (Prim Alpha)",
),
("Vertex Colored Texture Transparent", "Vertex Colored Texture Transparent", "Vertex Colored Texture Transparent"),
("Shaded Noise", "Shaded Noise", "Shaded Noise"),
(
"Vertex Colored Texture (No Vertex Alpha)",
"Vertex Colored Texture (No Vertex Alpha)",
"Vertex Colored Texture (No Vertex Alpha)",
),
]