2022-09-28 12:47:43 +00:00
|
|
|
""" 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
|
|
|
|
|
2022-10-03 18:12:21 +00:00
|
|
|
from .option_file_parser import MungeOptions
|
|
|
|
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-09-29 11:11:47 +00:00
|
|
|
import os
|
|
|
|
|
2022-09-28 12:47:43 +00:00
|
|
|
|
|
|
|
# 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)]
|
|
|
|
|
2022-10-04 02:55:19 +00:00
|
|
|
mats_visited = set()
|
|
|
|
|
2022-09-28 12:47:43 +00:00
|
|
|
for mat in mats:
|
2022-10-04 02:55:19 +00:00
|
|
|
|
2022-10-04 03:06:33 +00:00
|
|
|
if mat.name in mats_visited or not mat.swbf_msh_mat:
|
2022-10-04 02:55:19 +00:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
mats_visited.add(mat.name)
|
|
|
|
|
2022-10-04 03:06:33 +00:00
|
|
|
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")
|
2022-10-04 11:54:25 +00:00
|
|
|
mat.swbf_msh_mat.additive_transparency = (mat.blend_method == "ADDITIVE")
|
2022-10-04 03:06:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
# Below is all for filling the diffuse map/texture_0 fields
|
|
|
|
|
2022-09-28 12:47:43 +00:00
|
|
|
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']
|
|
|
|
|
2022-10-04 02:55:19 +00:00
|
|
|
stack = []
|
|
|
|
|
|
|
|
texture_node = None
|
|
|
|
|
|
|
|
current_socket = base_col
|
|
|
|
if base_col.is_linked:
|
|
|
|
stack.append(base_col.links[0].from_node)
|
|
|
|
|
|
|
|
while stack:
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-04 02:55:19 +00:00
|
|
|
curr_node = stack.pop()
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-04 02:55:19 +00:00
|
|
|
if curr_node.type == 'TEX_IMAGE':
|
|
|
|
texture_node = curr_node
|
|
|
|
break
|
|
|
|
else:
|
2022-10-04 13:54:28 +00:00
|
|
|
# Crude but good for now
|
2022-10-04 02:55:19 +00:00
|
|
|
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)
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-04 02:55:19 +00:00
|
|
|
|
|
|
|
if texture_node is not None:
|
|
|
|
|
|
|
|
tex_path = texture_node.image.filepath
|
|
|
|
|
|
|
|
tex_name = os.path.basename(tex_path)
|
|
|
|
|
|
|
|
i = tex_name.find('.')
|
2022-09-28 12:47:43 +00:00
|
|
|
|
|
|
|
# Get rid of trailing number in case one is present
|
|
|
|
if i > 0:
|
2022-10-04 02:55:19 +00:00
|
|
|
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
|
2022-09-28 12:47:43 +00:00
|
|
|
|
|
|
|
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...
|
2022-10-04 13:54:28 +00:00
|
|
|
# So much fun to write this, will probably do all render types by end of October
|
2022-09-28 12:47:43 +00:00
|
|
|
|
|
|
|
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."
|
|
|
|
)
|
2022-09-29 11:11:47 +00:00
|
|
|
|
|
|
|
fail_silently: BoolProperty(
|
|
|
|
name = "Fail Silently"
|
|
|
|
)
|
2022-09-28 12:47:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
material = bpy.data.materials.get(self.material_name, None)
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
if not material or not material.swbf_msh_mat:
|
|
|
|
return {'CANCELLED'}
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
mat_props = material.swbf_msh_mat
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-09-29 11:11:47 +00:00
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
texture_input_nodes = []
|
|
|
|
surface_output_nodes = []
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-04 01:16:59 +00:00
|
|
|
# Op will give up if no diffuse map is present.
|
|
|
|
# Eventually more nuance will be added for different
|
|
|
|
# rtypes
|
2022-10-02 19:55:47 +00:00
|
|
|
diffuse_texture_path = mat_props.diffuse_map
|
|
|
|
if diffuse_texture_path and os.path.exists(diffuse_texture_path):
|
2022-10-03 12:38:13 +00:00
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
material.use_nodes = True
|
|
|
|
material.node_tree.nodes.clear()
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
bsdf = material.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
|
2022-10-02 20:30:11 +00:00
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
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'])
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-02 20:30:11 +00:00
|
|
|
texture_input_nodes.append(texImage)
|
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
bsdf.inputs["Roughness"].default_value = 1.0
|
|
|
|
bsdf.inputs["Specular"].default_value = 0.0
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
material.use_backface_culling = not bool(mat_props.doublesided)
|
|
|
|
|
2022-10-04 13:54:28 +00:00
|
|
|
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")
|
2022-10-02 19:55:47 +00:00
|
|
|
|
2022-10-04 13:54:28 +00:00
|
|
|
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)
|
2022-10-02 19:55:47 +00:00
|
|
|
|
2022-10-04 01:16:59 +00:00
|
|
|
# Glow (adds another shader output)
|
2022-10-04 13:54:28 +00:00
|
|
|
else:
|
2022-10-02 19:55:47 +00:00
|
|
|
|
|
|
|
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])
|
2022-09-29 11:11:47 +00:00
|
|
|
|
2022-10-04 13:54:28 +00:00
|
|
|
surface_output_nodes.append(("Emission", emission))
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
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])
|
2022-09-28 12:47:43 +00:00
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
surfaces_output = mix
|
2022-10-02 19:55:47 +00:00
|
|
|
|
2022-10-04 01:16:59 +00:00
|
|
|
# Normal/bump mapping (needs more rendertype support!)
|
2022-10-03 16:07:52 +00:00
|
|
|
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)
|
|
|
|
|
2022-10-03 18:12:21 +00:00
|
|
|
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
|
2022-10-03 16:07:52 +00:00
|
|
|
|
2022-10-03 18:12:21 +00:00
|
|
|
material.node_tree.links.new(bsdf.inputs['Normal'], normalsOutputNode.outputs["Normal"])
|
2022-10-03 16:07:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
output = material.node_tree.nodes.new("ShaderNodeOutputMaterial")
|
|
|
|
material.node_tree.links.new(output.inputs['Surface'], surfaces_output.outputs[0])
|
2022-10-02 22:19:29 +00:00
|
|
|
|
2022-10-02 20:30:11 +00:00
|
|
|
|
2022-10-04 01:16:59 +00:00
|
|
|
|
|
|
|
# Scrolling
|
|
|
|
# This approach works 90% of the time, but notably produces very incorrect results
|
|
|
|
# on mus1_bldg_world_1,2,3
|
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
# Clear all anims in all cases
|
|
|
|
if material.node_tree.animation_data:
|
2022-10-04 13:54:28 +00:00
|
|
|
material.node_tree.animation_data_clear()
|
2022-10-02 20:30:11 +00:00
|
|
|
|
2022-10-02 22:19:29 +00:00
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
if "SCROLL" in mat_props.rendertype:
|
|
|
|
uv_input = material.node_tree.nodes.new("ShaderNodeUVMap")
|
2022-10-02 22:19:29 +00:00
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
vector_add = material.node_tree.nodes.new("ShaderNodeVectorMath")
|
2022-10-02 22:19:29 +00:00
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
# 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)
|
2022-10-02 20:30:11 +00:00
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
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)
|
2022-10-02 20:30:11 +00:00
|
|
|
|
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
material.node_tree.links.new(vector_add.inputs[0], uv_input.outputs[0])
|
2022-10-02 20:30:11 +00:00
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
for texture_node in texture_input_nodes:
|
|
|
|
material.node_tree.links.new(texture_node.inputs["Vector"], vector_add.outputs[0])
|
2022-10-02 20:30:11 +00:00
|
|
|
|
2022-10-03 12:38:13 +00:00
|
|
|
# Don't know how to set interpolation when adding keyframes
|
|
|
|
# so we must do it after the fact
|
2022-10-04 13:54:28 +00:00
|
|
|
if material.node_tree.animation_data and material.node_tree.animation_data.action:
|
2022-10-03 12:38:13 +00:00
|
|
|
for fcurve in material.node_tree.animation_data.action.fcurves:
|
|
|
|
for kf in fcurve.keyframe_points.values():
|
|
|
|
kf.interpolation = 'LINEAR'
|
2022-10-02 20:30:11 +00:00
|
|
|
|
2022-10-02 19:55:47 +00:00
|
|
|
'''
|
|
|
|
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.")
|
|
|
|
'''
|
|
|
|
|
2022-09-28 12:47:43 +00:00
|
|
|
return {'FINISHED'}
|
|
|
|
|