import os import subprocess from typing import Dict, List, Tuple import bpy from bpy.types import Material from bpy.types import Object import lnx.api import lnx.assets as assets import lnx.exporter import lnx.log as log import lnx.material.cycles as cycles import lnx.material.make_decal as make_decal import lnx.material.make_depth as make_depth import lnx.material.make_mesh as make_mesh import lnx.material.make_overlay as make_overlay import lnx.material.make_transluc as make_transluc import lnx.material.make_refract as make_refract import lnx.material.make_voxel as make_voxel import lnx.material.mat_state as mat_state import lnx.material.mat_utils as mat_utils from lnx.material.shader import Shader, ShaderContext, ShaderData import lnx.utils if lnx.is_reload(__name__): lnx.api = lnx.reload_module(lnx.api) assets = lnx.reload_module(assets) lnx.exporter = lnx.reload_module(lnx.exporter) log = lnx.reload_module(log) cycles = lnx.reload_module(cycles) make_decal = lnx.reload_module(make_decal) make_depth = lnx.reload_module(make_depth) make_mesh = lnx.reload_module(make_mesh) make_overlay = lnx.reload_module(make_overlay) make_transluc = lnx.reload_module(make_transluc) make_voxel = lnx.reload_module(make_voxel) mat_state = lnx.reload_module(mat_state) mat_utils = lnx.reload_module(mat_utils) lnx.material.shader = lnx.reload_module(lnx.material.shader) from lnx.material.shader import Shader, ShaderContext, ShaderData lnx.utils = lnx.reload_module(lnx.utils) else: lnx.enable_reload(__name__) rpass_hook = None def build(material: Material, mat_users: Dict[Material, List[Object]], mat_lnxusers) -> Tuple: mat_state.mat_users = mat_users mat_state.mat_lnxusers = mat_lnxusers mat_state.material = material mat_state.nodes = material.node_tree.nodes mat_state.data = ShaderData(material) mat_state.output_node = cycles.node_by_type(mat_state.nodes, 'OUTPUT_MATERIAL') if mat_state.output_node is None: # Place empty material output to keep compiler happy.. mat_state.output_node = mat_state.nodes.new('ShaderNodeOutputMaterial') wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() rpasses = mat_utils.get_rpasses(material) matname = lnx.utils.safesrc(lnx.utils.asset_name(material)) rel_path = lnx.utils.build_dir() + '/compiled/Shaders/' full_path = lnx.utils.get_fp() + '/' + rel_path if not os.path.exists(full_path): os.makedirs(full_path) make_instancing_and_skinning(material, mat_users) bind_constants = dict() bind_textures = dict() for rp in rpasses: car = [] bind_constants[rp] = car mat_state.bind_constants = car tar = [] bind_textures[rp] = tar mat_state.bind_textures = tar con = None if rpdat.rp_driver != 'Leenkx' and lnx.api.drivers[rpdat.rp_driver]['make_rpass'] is not None: con = lnx.api.drivers[rpdat.rp_driver]['make_rpass'](rp) if con is not None: pass elif rp == 'mesh': con = make_mesh.make(rp, rpasses) elif rp == 'shadowmap': con = make_depth.make(rp, rpasses, shadowmap=True) elif rp == 'shadowmap_transparent': con = make_depth.make(rp, rpasses, shadowmap=True, shadowmap_transparent=True) elif rp == 'translucent': con = make_transluc.make(rp) elif rp == 'refraction': con = make_refract.make(rp) elif rp == 'overlay': con = make_overlay.make(rp) elif rp == 'decal': con = make_decal.make(rp) elif rp == 'depth': con = make_depth.make(rp, rpasses) elif rp == 'voxel': con = make_voxel.make(rp) elif rpass_hook is not None: con = rpass_hook(rp) write_shaders(rel_path, con, rp, matname) shader_data_name = matname + '_data' if wrd.lnx_single_data_file: if 'shader_datas' not in lnx.exporter.current_output: lnx.exporter.current_output['shader_datas'] = [] lnx.exporter.current_output['shader_datas'].append(mat_state.data.get()['shader_datas'][0]) else: lnx.utils.write_lnx(full_path + '/' + matname + '_data.lnx', mat_state.data.get()) shader_data_path = lnx.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.lnx' assets.add_shader_data(shader_data_path) return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures def write_shaders(rel_path: str, con: ShaderContext, rpass: str, matname: str) -> None: keep_cache = mat_state.material.lnx_cached write_shader(rel_path, con.vert, 'vert', rpass, matname, keep_cache=keep_cache) write_shader(rel_path, con.frag, 'frag', rpass, matname, keep_cache=keep_cache) write_shader(rel_path, con.geom, 'geom', rpass, matname, keep_cache=keep_cache) write_shader(rel_path, con.tesc, 'tesc', rpass, matname, keep_cache=keep_cache) write_shader(rel_path, con.tese, 'tese', rpass, matname, keep_cache=keep_cache) def write_shader(rel_path: str, shader: Shader, ext: str, rpass: str, matname: str, keep_cache=True) -> None: if shader is None or shader.is_linked: return # TODO: blend context if rpass == 'mesh' and mat_state.material.lnx_blending: rpass = 'blend' file_ext = '.glsl' if shader.noprocessing: # Use hlsl directly hlsl_dir = lnx.utils.build_dir() + '/compiled/Hlsl/' if not os.path.exists(hlsl_dir): os.makedirs(hlsl_dir) file_ext = '.hlsl' rel_path = rel_path.replace('/compiled/Shaders/', '/compiled/Hlsl/') shader_file = matname + '_' + rpass + '.' + ext + file_ext shader_path = lnx.utils.get_fp() + '/' + rel_path + '/' + shader_file assets.add_shader(shader_path) if not os.path.isfile(shader_path) or not keep_cache: with open(shader_path, 'w') as f: f.write(shader.get()) if shader.noprocessing: cwd = os.getcwd() os.chdir(lnx.utils.get_fp() + '/' + rel_path) hlslbin_path = lnx.utils.get_sdk_path() + '/lib/leenkx_tools/hlslbin/hlslbin.exe' prof = 'vs_5_0' if ext == 'vert' else 'ps_5_0' if ext == 'frag' else 'gs_5_0' # noprocessing flag - gets renamed to .d3d11 args = [hlslbin_path.replace('/', '\\').replace('\\\\', '\\'), shader_file, shader_file[:-4] + 'glsl', prof] if ext == 'vert': args.append('-i') args.append('pos') proc = subprocess.call(args) os.chdir(cwd) def make_instancing_and_skinning(mat: Material, mat_users: Dict[Material, List[Object]]) -> None: """Build material with instancing or skinning if enabled. If the material is a custom material, only validation checks for instancing are performed.""" global_elems = [] if mat_users is not None and mat in mat_users: # Whether there are both an instanced object and a not instanced object with this material instancing_usage = [False, False] mat_state.uses_instancing = False for bo in mat_users[mat]: if mat.lnx_custom_material == '': # Morph Targets if lnx.utils.export_morph_targets(bo): global_elems.append({'name': 'morph', 'data': 'short2norm'}) # GPU Skinning if lnx.utils.export_bone_data(bo): global_elems.append({'name': 'bone', 'data': 'short4norm'}) global_elems.append({'name': 'weight', 'data': 'short4norm'}) # Instancing inst = bo.lnx_instanced if inst != 'Off' or mat.lnx_particle_flag: instancing_usage[0] = True mat_state.uses_instancing = True if mat.lnx_custom_material == '': global_elems.append({'name': 'ipos', 'data': 'float3'}) if 'Rot' in inst: global_elems.append({'name': 'irot', 'data': 'float3'}) if 'Scale' in inst: global_elems.append({'name': 'iscl', 'data': 'float3'}) elif inst == 'Off': # Ignore children of instanced objects, they are instanced even when set to 'Off' instancing_usage[1] = bo.parent is None or bo.parent.lnx_instanced == 'Off' if instancing_usage[0] and instancing_usage[1]: # Display a warning for invalid instancing configurations # See https://github.com/leenkx3d/leenkx/issues/2072 log.warn(f'Material "{mat.name}" has both instanced and not instanced objects, objects might flicker!') if mat.lnx_custom_material == '': mat_state.data.global_elems = global_elems