SWBF-msh-Blender-IO/addons/io_scene_swbf_msh/msh_material_operators.py

323 lines
12 KiB
Python

""" Operators for basic emulation and mapping of SWBF material system in Blender.
Only relevant if the builtin Eevee renderer is being used! """
import bpy
from .msh_material_properties import *
from math import sqrt
from bpy.props import BoolProperty, EnumProperty, StringProperty
from bpy.types import Operator, Menu
from .option_file_parser import MungeOptions
import os
# FillSWBFMaterialProperties
# Iterates through all material slots of all selected
# objects and fills basic SWBF material properties
# from any Principled BSDF nodes it finds.
class FillSWBFMaterialProperties(bpy.types.Operator):
bl_idname = "swbf_msh.fill_mat_props"
bl_label = "Fill SWBF Material Properties"
bl_description = ("Fill in SWBF properties of all materials used by selected objects.\n"
"Only considers materials that use nodes.\n"
"Please see 'Materials > Materials Operators' in the docs for more details.")
def execute(self, context):
slots = sum([list(ob.material_slots) for ob in bpy.context.selected_objects if ob.type == 'MESH'],[])
mats = [slot.material for slot in slots if (slot.material and slot.material.node_tree)]
mats_visited = set()
for mat in mats:
if mat.name in mats_visited or not mat.swbf_msh_mat:
continue
else:
mats_visited.add(mat.name)
mat.swbf_msh_mat.doublesided = not mat.use_backface_culling
mat.swbf_msh_mat.hardedged_transparency = (mat.blend_method == "CLIP")
mat.swbf_msh_mat.blended_transparency = (mat.blend_method == "BLEND")
mat.swbf_msh_mat.additive_transparency = (mat.blend_method == "ADDITIVE")
# Below is all for filling the diffuse map/texture_0 fields
try:
for BSDF_node in [n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED']:
base_col = BSDF_node.inputs['Base Color']
stack = []
texture_node = None
current_socket = base_col
if base_col.is_linked:
stack.append(base_col.links[0].from_node)
while stack:
curr_node = stack.pop()
if curr_node.type == 'TEX_IMAGE':
texture_node = curr_node
break
else:
# Crude but good for now
next_nodes = []
for node_input in curr_node.inputs:
for link in node_input.links:
next_nodes.append(link.from_node)
# reversing it so we go from up to down
stack += reversed(next_nodes)
if texture_node is not None:
tex_path = texture_node.image.filepath
tex_name = os.path.basename(tex_path)
i = tex_name.find('.')
# Get rid of trailing number in case one is present
if i > 0:
tex_name = tex_name[0:i] + ".tga"
refined_tex_path = os.path.join(os.path.dirname(tex_path), tex_name)
mat.swbf_msh_mat.diffuse_map = refined_tex_path
mat.swbf_msh_mat.texture_0 = refined_tex_path
break
except:
# Many chances for null ref exceptions. None if user reads doc section...
pass
return {'FINISHED'}
class VIEW3D_MT_SWBF(bpy.types.Menu):
bl_label = "SWBF"
def draw(self, _context):
layout = self.layout
layout.operator("swbf_msh.fill_mat_props", text="Fill SWBF Material Properties")
def draw_matfill_menu(self, context):
layout = self.layout
layout.separator()
layout.menu("VIEW3D_MT_SWBF")
# GenerateMaterialNodesFromSWBFProperties
# Creates shader nodes to emulate SWBF material properties.
# Will probably only support for a narrow subset of properties...
# So much fun to write this, will probably do all render types by end of October
class GenerateMaterialNodesFromSWBFProperties(bpy.types.Operator):
bl_idname = "swbf_msh.generate_material_nodes"
bl_label = "Generate Nodes"
bl_description= """Generate Cycles shader nodes from SWBF material properties.
The nodes generated are meant to give one a general idea
of how the material would look ingame. They cannot
to provide an exact emulation"""
material_name: StringProperty(
name = "Material Name",
description = "Name of material whose SWBF properties the generated nodes will emulate."
)
fail_silently: BoolProperty(
name = "Fail Silently"
)
def execute(self, context):
material = bpy.data.materials.get(self.material_name, None)
if not material or not material.swbf_msh_mat:
return {'CANCELLED'}
mat_props = material.swbf_msh_mat
texture_input_nodes = []
surface_output_nodes = []
# Op will give up if no diffuse map is present.
# Eventually more nuance will be added for different
# rtypes
diffuse_texture_path = mat_props.diffuse_map
if diffuse_texture_path and os.path.exists(diffuse_texture_path):
material.use_nodes = True
material.node_tree.nodes.clear()
bsdf = material.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
texImage = material.node_tree.nodes.new('ShaderNodeTexImage')
texImage.image = bpy.data.images.load(diffuse_texture_path)
texImage.image.alpha_mode = 'CHANNEL_PACKED'
material.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color'])
texture_input_nodes.append(texImage)
bsdf.inputs["Roughness"].default_value = 1.0
bsdf.inputs["Specular"].default_value = 0.0
material.use_backface_culling = not bool(mat_props.doublesided)
surface_output_nodes.append(('BSDF', bsdf))
if not mat_props.glow:
if mat_props.hardedged_transparency:
material.blend_method = "CLIP"
material.node_tree.links.new(bsdf.inputs['Alpha'], texImage.outputs['Alpha'])
elif mat_props.blended_transparency:
material.blend_method = "BLEND"
material.node_tree.links.new(bsdf.inputs['Alpha'], texImage.outputs['Alpha'])
elif mat_props.additive_transparency:
# most complex
transparent_bsdf = material.node_tree.nodes.new("ShaderNodeBsdfTransparent")
add_shader = material.node_tree.nodes.new("ShaderNodeAddShader")
material.node_tree.links.new(add_shader.inputs[0], bsdf.outputs["BSDF"])
material.node_tree.links.new(add_shader.inputs[1], transparent_bsdf.outputs["BSDF"])
surface_output_nodes[0] = ('Shader', add_shader)
# Glow (adds another shader output)
else:
emission = material.node_tree.nodes.new("ShaderNodeEmission")
material.node_tree.links.new(emission.inputs['Color'], texImage.outputs['Color'])
emission_strength_multiplier = material.node_tree.nodes.new("ShaderNodeMath")
emission_strength_multiplier.operation = 'MULTIPLY'
emission_strength_multiplier.inputs[1].default_value = 32.0
material.node_tree.links.new(emission_strength_multiplier.inputs[0], texImage.outputs['Alpha'])
material.node_tree.links.new(emission.inputs['Strength'], emission_strength_multiplier.outputs[0])
surface_output_nodes.append(("Emission", emission))
surfaces_output = None
if (len(surface_output_nodes) == 1):
surfaces_output = surface_output_nodes[0][1]
else:
mix = material.node_tree.nodes.new("ShaderNodeMixShader")
material.node_tree.links.new(mix.inputs[1], surface_output_nodes[0][1].outputs[0])
material.node_tree.links.new(mix.inputs[2], surface_output_nodes[1][1].outputs[0])
surfaces_output = mix
# Normal/bump mapping (needs more rendertype support!)
if "NORMALMAP" in mat_props.rendertype and mat_props.normal_map and os.path.exists(mat_props.normal_map):
normalMapTexImage = material.node_tree.nodes.new('ShaderNodeTexImage')
normalMapTexImage.image = bpy.data.images.load(mat_props.normal_map)
normalMapTexImage.image.alpha_mode = 'CHANNEL_PACKED'
normalMapTexImage.image.colorspace_settings.name = 'Non-Color'
texture_input_nodes.append(normalMapTexImage)
options = MungeOptions(mat_props.normal_map + ".option")
if options.get_bool("bumpmap"):
# First we must convert the RGB data to brightness
rgb_to_bw_node = material.node_tree.nodes.new("ShaderNodeRGBToBW")
material.node_tree.links.new(rgb_to_bw_node.inputs["Color"], normalMapTexImage.outputs["Color"])
# Now create a bump map node (perhaps we could also use this with normals and just plug color into normal input?)
bumpMapNode = material.node_tree.nodes.new('ShaderNodeBump')
bumpMapNode.inputs["Distance"].default_value = options.get_float("bumpscale", default=1.0)
material.node_tree.links.new(bumpMapNode.inputs["Height"], rgb_to_bw_node.outputs["Val"])
normalsOutputNode = bumpMapNode
else:
normalMapNode = material.node_tree.nodes.new('ShaderNodeNormalMap')
material.node_tree.links.new(normalMapNode.inputs["Color"], normalMapTexImage.outputs["Color"])
normalsOutputNode = normalMapNode
material.node_tree.links.new(bsdf.inputs['Normal'], normalsOutputNode.outputs["Normal"])
output = material.node_tree.nodes.new("ShaderNodeOutputMaterial")
material.node_tree.links.new(output.inputs['Surface'], surfaces_output.outputs[0])
# Scrolling
# This approach works 90% of the time, but notably produces very incorrect results
# on mus1_bldg_world_1,2,3
# Clear all anims in all cases
if material.node_tree.animation_data:
material.node_tree.animation_data_clear()
if "SCROLL" in mat_props.rendertype:
uv_input = material.node_tree.nodes.new("ShaderNodeUVMap")
vector_add = material.node_tree.nodes.new("ShaderNodeVectorMath")
# Add keyframes
scroll_per_sec_divisor = 255.0
frame_step = 60.0
fps = bpy.context.scene.render.fps
for i in range(2):
vector_add.inputs[1].default_value[0] = i * mat_props.scroll_speed_u * frame_step / scroll_per_sec_divisor
vector_add.inputs[1].keyframe_insert("default_value", index=0, frame=i * frame_step * fps)
vector_add.inputs[1].default_value[1] = i * mat_props.scroll_speed_v * frame_step / scroll_per_sec_divisor
vector_add.inputs[1].keyframe_insert("default_value", index=1, frame=i * frame_step * fps)
material.node_tree.links.new(vector_add.inputs[0], uv_input.outputs[0])
for texture_node in texture_input_nodes:
material.node_tree.links.new(texture_node.inputs["Vector"], vector_add.outputs[0])
# Don't know how to set interpolation when adding keyframes
# so we must do it after the fact
if material.node_tree.animation_data and material.node_tree.animation_data.action:
for fcurve in material.node_tree.animation_data.action.fcurves:
for kf in fcurve.keyframe_points.values():
kf.interpolation = 'LINEAR'
'''
else:
# Todo: figure out some way to raise an error but continue operator execution...
if self.fail_silently:
return {'CANCELLED'}
else:
raise RuntimeError(f"Diffuse texture at path: '{diffuse_texture_path}' was not found.")
'''
return {'FINISHED'}