from typing import Dict, List import bpy from bpy.types import Material from bpy.types import Object import lnx.log as log import lnx.material.cycles as cycles import lnx.material.make_shader as make_shader import lnx.material.mat_batch as mat_batch import lnx.material.mat_state as mat_state import lnx.material.mat_utils as mat_utils import lnx.node_utils import lnx.utils if lnx.is_reload(__name__): log = lnx.reload_module(log) cycles = lnx.reload_module(cycles) make_shader = lnx.reload_module(make_shader) mat_batch = lnx.reload_module(mat_batch) mat_state = lnx.reload_module(mat_state) mat_utils = lnx.reload_module(mat_utils) lnx.node_utils = lnx.reload_module(lnx.node_utils) lnx.utils = lnx.reload_module(lnx.utils) else: lnx.enable_reload(__name__) def glsl_value(val): if str(type(val)) == "": res = [] for v in val: res.append(v) return res else: return val def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]], mat_lnxusers) -> tuple: wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() mat_state.features = {} # Texture caching for material batching batch_cached_textures = [] needs_sss = material_needs_sss(material) if needs_sss and rpdat.rp_sss_state != 'Off' and '_SSS' not in wrd.world_defs: # Must be set before calling make_shader.build() wrd.world_defs += '_SSS' # No batch - shader data per material if material.lnx_custom_material != '': rpasses = ['mesh'] con = {'vertex_elements': []} con['vertex_elements'].append({'name': 'pos', 'data': 'short4norm'}) con['vertex_elements'].append({'name': 'nor', 'data': 'short2norm'}) con['vertex_elements'].append({'name': 'tex', 'data': 'short2norm'}) con['vertex_elements'].append({'name': 'tex1', 'data': 'short2norm'}) sd = {'contexts': [con]} shader_data_name = material.lnx_custom_material bind_constants = {'mesh': []} bind_textures = {'mesh': []} mat_uses_sss = False make_shader.make_instancing_and_skinning(material, mat_users) for idx, item in enumerate(material.lnx_bind_textures_list): if item.uniform_name == '': log.warn(f'Material "{material.name}": skipping export of bind texture at slot {idx + 1} with empty uniform name') continue if item.image is not None: tex = cycles.make_texture(item.image, item.uniform_name, material.name, 'Linear', 'REPEAT') if tex is None: continue bind_textures['mesh'].append(tex) else: log.warn(f'Material "{material.name}": skipping export of bind texture at slot {idx + 1} ("{item.uniform_name}") with no image selected') elif not wrd.lnx_batch_materials or material.name.startswith('lnxdefault'): rpasses, shader_data, shader_data_name, bind_constants, bind_textures, mat_uses_sss = make_shader.build(material, mat_users, mat_lnxusers) sd = shader_data.sd else: result = mat_batch.get(material) rpasses, shader_data, shader_data_name, bind_constants, bind_textures, mat_uses_sss = result sd = shader_data.sd sss_used = False # Material for rp in rpasses: c = { 'name': rp, 'bind_constants': [] + bind_constants[rp], 'bind_textures': [] + bind_textures[rp], 'depth_read': material.lnx_depth_read, } mat_data['contexts'].append(c) if rp == 'mesh': c['bind_constants'].append({'name': 'receiveShadow', 'boolValue': material.lnx_receive_shadow}) if material.lnx_material_id != 0: c['bind_constants'].append({'name': 'materialID', 'intValue': material.lnx_material_id}) if material.lnx_material_id == 2: wrd.world_defs += '_Hair' elif rpdat.rp_sss_state != 'Off': const = {'name': 'materialID'} # Use per-material SSS flag from shader build if mat_uses_sss: const['intValue'] = 2 sss_used = True if '_SSS' not in wrd.world_defs: wrd.world_defs += '_SSS' else: const['intValue'] = 0 c['bind_constants'].append(const) # extended BRDF parameters as bind_constants if rpdat.rp_renderer == 'Deferred': feats = mat_state.features def try_float(val_str, default=1.0): try: return float(val_str) except (ValueError, TypeError): return default has_ext_brdf = False if feats.get('clearcoat', '0.0') not in ('0.0', '0', ''): c['bind_constants'].append({'name': 'clearcoat', 'floatValue': try_float(feats.get('clearcoat', '1.0'))}) c['bind_constants'].append({'name': 'clearcoatRough', 'floatValue': try_float(feats.get('clearcoatRough', '0.03'), 0.03)}) c['bind_constants'].append({'name': 'coatIOR', 'floatValue': try_float(feats.get('coatIOR', '1.5'), 1.5)}) c['bind_constants'].append({'name': 'coatTintR', 'floatValue': try_float(feats.get('coatTintR', '1.0'), 1.0)}) c['bind_constants'].append({'name': 'coatTintG', 'floatValue': try_float(feats.get('coatTintG', '1.0'), 1.0)}) c['bind_constants'].append({'name': 'coatTintB', 'floatValue': try_float(feats.get('coatTintB', '1.0'), 1.0)}) if '_ClearCoat' not in wrd.world_defs: wrd.world_defs += '_ClearCoat' has_ext_brdf = True if feats.get('sheen', '0.0') not in ('0.0', '0', ''): c['bind_constants'].append({'name': 'sheen', 'floatValue': try_float(feats.get('sheen', '1.0'))}) c['bind_constants'].append({'name': 'sheenRough', 'floatValue': try_float(feats.get('sheenRough', '0.5'), 0.5)}) c['bind_constants'].append({'name': 'sheenTintR', 'floatValue': try_float(feats.get('sheenTintR', '1.0'), 1.0)}) c['bind_constants'].append({'name': 'sheenTintG', 'floatValue': try_float(feats.get('sheenTintG', '1.0'), 1.0)}) c['bind_constants'].append({'name': 'sheenTintB', 'floatValue': try_float(feats.get('sheenTintB', '1.0'), 1.0)}) if '_Sheen' not in wrd.world_defs: wrd.world_defs += '_Sheen' has_ext_brdf = True if feats.get('subsurface', '0.0') not in ('0.0', '0', '') and rpdat.rp_sss_state != 'Off' and not mat_uses_sss: c['bind_constants'].append({'name': 'subsurface', 'floatValue': try_float(feats.get('subsurface', '1.0'))}) c['bind_constants'].append({'name': 'subsurfaceAnisotropy', 'floatValue': try_float(feats.get('subsurfaceAnisotropy', '0.0'), 0.0)}) c['bind_constants'].append({'name': 'subsurfaceRadiusR', 'floatValue': try_float(feats.get('subsurfaceRadiusR', '1.0'), 1.0)}) c['bind_constants'].append({'name': 'subsurfaceRadiusG', 'floatValue': try_float(feats.get('subsurfaceRadiusG', '0.2'), 0.2)}) c['bind_constants'].append({'name': 'subsurfaceRadiusB', 'floatValue': try_float(feats.get('subsurfaceRadiusB', '0.1'), 0.1)}) c['bind_constants'].append({'name': 'subsurfaceColorR', 'floatValue': try_float(feats.get('subsurfaceColorR', '0.8'), 0.8)}) c['bind_constants'].append({'name': 'subsurfaceColorG', 'floatValue': try_float(feats.get('subsurfaceColorG', '0.8'), 0.8)}) c['bind_constants'].append({'name': 'subsurfaceColorB', 'floatValue': try_float(feats.get('subsurfaceColorB', '0.8'), 0.8)}) if '_Subsurface' not in wrd.world_defs: wrd.world_defs += '_Subsurface' has_ext_brdf = True if feats.get('anisotropy', '0.0') not in ('0.0', '0', ''): c['bind_constants'].append({'name': 'anisotropy', 'floatValue': try_float(feats.get('anisotropy', '1.0'))}) c['bind_constants'].append({'name': 'anisoRot', 'floatValue': try_float(feats.get('anisoRot', '0.0'), 0.0)}) if '_Anisotropy' not in wrd.world_defs: wrd.world_defs += '_Anisotropy' has_ext_brdf = True if feats.get('transmission', '0.0') not in ('0.0', '0', ''): c['bind_constants'].append({'name': 'transmission', 'floatValue': try_float(feats.get('transmission', '1.0'))}) c['bind_constants'].append({'name': 'transmissionRough', 'floatValue': try_float(feats.get('transmissionRough', '0.0'), 0.0)}) c['bind_constants'].append({'name': 'ior', 'floatValue': try_float(feats.get('ior', '1.45'), 1.45)}) c['bind_constants'].append({'name': 'thinWall', 'floatValue': try_float(feats.get('thinWall', '0.0'), 0.0)}) if '_Transmission' not in wrd.world_defs: wrd.world_defs += '_Transmission' has_ext_brdf = True if has_ext_brdf and '_ExtBRDF' not in wrd.world_defs: wrd.world_defs += '_ExtBRDF' # assign materialID for extended BRDF if not already set # coexist with Scene.hx for last materialID in bind_constants if has_ext_brdf and material.lnx_material_id == 0: if mat_state.next_ext_mat_id <= 15: ext_id = mat_state.next_ext_mat_id mat_state.next_ext_mat_id += 1 c['bind_constants'].append({'name': 'materialID', 'intValue': ext_id}) else: log.warn(f'material "{material.name}": extended BRDF material limit (15) exceeded, extended BRDF will be disabled for this material') # TODO: Mesh only material batching if wrd.lnx_batch_materials: # Set textures uniforms if len(c['bind_textures']) > 0: c['bind_textures'] = [] for node in material.node_tree.nodes: if node.type == 'TEX_IMAGE': tex_name = lnx.utils.safesrc(node.name) tex = cycles.make_texture_from_image_node(node, tex_name) # Empty texture if tex is None: tex = {'name': tex_name, 'file': ''} c['bind_textures'].append(tex) batch_cached_textures = c['bind_textures'] # Set marked inputs as uniforms for node in material.node_tree.nodes: for inp in node.inputs: if inp.is_uniform: uname = lnx.utils.safesrc(inp.node.name) + lnx.utils.safesrc(inp.name) # Merge with cycles module c['bind_constants'].append({'name': uname, cycles.glsl_type(inp.type)+'Value': glsl_value(inp.default_value)}) elif rp == 'translucent' or rp == 'refraction': c['bind_constants'].append({'name': 'receiveShadow', 'boolValue': material.lnx_receive_shadow}) elif rp == 'shadowmap': if wrd.lnx_batch_materials: if len(c['bind_textures']) > 0: c['bind_textures'] = batch_cached_textures if wrd.lnx_single_data_file: mat_data['shader'] = shader_data_name else: # Make sure that custom materials are not expected to be in .arm format ext = '' if wrd.lnx_minimize and material.lnx_custom_material == "" else '.json' mat_data['shader'] = shader_data_name + ext + '/' + shader_data_name return sd, rpasses, sss_used def material_needs_sss(material: Material) -> bool: """Check whether the given material requires SSS.""" for sss_node in lnx.node_utils.iter_nodes_by_type(material.node_tree, 'SUBSURFACE_SCATTERING'): if sss_node is not None and sss_node.outputs[0].is_linked: return True for principled_node in lnx.node_utils.iter_nodes_by_type(material.node_tree, 'BSDF_PRINCIPLED'): if principled_node is not None and principled_node.outputs[0].is_linked: sss_input = principled_node.inputs.get('Subsurface Weight') or principled_node.inputs.get('Subsurface') if sss_input is not None and (sss_input.is_linked or sss_input.default_value > 0.0): return True for sss_node in mat_utils.iter_nodes_leenkxpbr(material.node_tree): if sss_node is not None and sss_node.outputs[0].is_linked and (sss_node.inputs[8].is_linked or sss_node.inputs[8].default_value != 0.0): return True return False