2025-01-22 16:18:30 +01:00
|
|
|
import os
|
2026-08-27 15:14:57 -07:00
|
|
|
import re
|
2025-01-22 16:18:30 +01:00
|
|
|
import subprocess
|
2026-08-27 15:14:57 -07:00
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
2025-01-22 16:18:30 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-08-27 15:14:57 -07:00
|
|
|
def cache_hit(cached, wrd, full_path, matname):
|
|
|
|
|
mat_state.features = dict(cached['features'])
|
|
|
|
|
mat_state.emission_type = cached['emission_type']
|
|
|
|
|
mat_state.texture_grad = cached['texture_grad']
|
|
|
|
|
mat_state.data = cached['shader_data']
|
|
|
|
|
|
|
|
|
|
if cached['world_defs_delta']:
|
|
|
|
|
for define in re.findall(r'_[A-Za-z0-9]+', cached['world_defs_delta']):
|
|
|
|
|
assets.add_world_def(wrd, define)
|
|
|
|
|
|
|
|
|
|
for d in cached['khafile_defs_delta']:
|
|
|
|
|
assets.add_khafile_def(d)
|
|
|
|
|
|
|
|
|
|
for shader_path in cached['shader_paths']:
|
|
|
|
|
assets.add_shader(shader_path)
|
|
|
|
|
|
|
|
|
|
shader_data_name = cached['shader_data_name']
|
|
|
|
|
if wrd.lnx_single_data_file:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
shader_data_path = lnx.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.lnx'
|
|
|
|
|
assets.add_shader_data(shader_data_path)
|
|
|
|
|
|
|
|
|
|
return cached['rpasses'], mat_state.data, shader_data_name, cached['bind_constants'], cached['bind_textures']
|
|
|
|
|
|
|
|
|
|
|
2025-01-22 16:18:30 +01:00
|
|
|
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')
|
2026-02-24 17:35:26 -08:00
|
|
|
|
2025-01-22 16:18:30 +01:00
|
|
|
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)
|
|
|
|
|
|
2026-08-27 15:14:57 -07:00
|
|
|
cache_key = None
|
|
|
|
|
cached = None
|
|
|
|
|
if material.signature:
|
|
|
|
|
global_elems_key = tuple((e['name'], e['data']) for e in mat_state.data.global_elems)
|
|
|
|
|
cache_key = (material.signature, mat_state.uses_instancing, tuple(rpasses), global_elems_key)
|
|
|
|
|
cached = mat_state.material_cache.get(cache_key)
|
|
|
|
|
|
|
|
|
|
if cached is not None:
|
|
|
|
|
return cache_hit(cached, wrd, full_path, matname)
|
|
|
|
|
|
|
|
|
|
world_defs_before = wrd.world_defs
|
|
|
|
|
khafile_defs_before = set(assets.khafile_defs)
|
|
|
|
|
|
2025-01-22 16:18:30 +01:00
|
|
|
bind_constants = dict()
|
|
|
|
|
bind_textures = dict()
|
2026-08-27 15:14:57 -07:00
|
|
|
all_shader_paths = []
|
2025-01-22 16:18:30 +01:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2026-08-27 15:14:57 -07:00
|
|
|
if con is not None:
|
|
|
|
|
rp_shader_paths = write_shaders(rel_path, con, rp, matname)
|
|
|
|
|
all_shader_paths.extend(rp_shader_paths)
|
2025-01-22 16:18:30 +01:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2026-08-27 15:14:57 -07:00
|
|
|
if cache_key is not None:
|
|
|
|
|
world_defs_delta = wrd.world_defs[len(world_defs_before):]
|
|
|
|
|
khafile_defs_delta = [d for d in assets.khafile_defs if d not in khafile_defs_before]
|
|
|
|
|
mat_state.material_cache[cache_key] = {
|
|
|
|
|
'shader_data': mat_state.data,
|
|
|
|
|
'shader_data_name': shader_data_name,
|
|
|
|
|
'bind_constants': bind_constants,
|
|
|
|
|
'bind_textures': bind_textures,
|
|
|
|
|
'features': dict(mat_state.features),
|
|
|
|
|
'emission_type': mat_state.emission_type,
|
|
|
|
|
'texture_grad': mat_state.texture_grad,
|
|
|
|
|
'shader_paths': all_shader_paths,
|
|
|
|
|
'world_defs_delta': world_defs_delta,
|
|
|
|
|
'khafile_defs_delta': khafile_defs_delta,
|
|
|
|
|
'rpasses': rpasses,
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 02:04:01 -07:00
|
|
|
return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures
|
2025-01-22 16:18:30 +01:00
|
|
|
|
|
|
|
|
|
2026-08-27 15:14:57 -07:00
|
|
|
def write_shaders(rel_path: str, con: ShaderContext, rpass: str, matname: str) -> List[str]:
|
2025-01-22 16:18:30 +01:00
|
|
|
keep_cache = mat_state.material.lnx_cached
|
2026-08-27 15:14:57 -07:00
|
|
|
shaders = [con.vert, con.frag, con.geom, con.tesc, con.tese]
|
|
|
|
|
exts = ['vert', 'frag', 'geom', 'tesc', 'tese']
|
|
|
|
|
shader_paths = []
|
|
|
|
|
|
|
|
|
|
write_tasks = []
|
|
|
|
|
for shader, ext in zip(shaders, exts):
|
|
|
|
|
if shader is None or shader.is_linked:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
validation_issues = shader.validate()
|
|
|
|
|
if validation_issues:
|
|
|
|
|
for issue in validation_issues:
|
|
|
|
|
log.warn(f"Shader validation issue in {matname}_{rpass}.{ext}: {issue}. ")
|
|
|
|
|
|
|
|
|
|
output_rpass = rpass
|
|
|
|
|
if output_rpass == 'mesh' and mat_state.material.lnx_blending:
|
|
|
|
|
output_rpass = 'blend'
|
|
|
|
|
|
|
|
|
|
output_ext = '.glsl'
|
|
|
|
|
output_rel_path = rel_path
|
|
|
|
|
if shader.noprocessing:
|
|
|
|
|
hlsl_dir = lnx.utils.build_dir() + '/compiled/Hlsl/'
|
|
|
|
|
os.makedirs(hlsl_dir, exist_ok=True)
|
|
|
|
|
output_ext = '.hlsl'
|
|
|
|
|
output_rel_path = rel_path.replace('/compiled/Shaders/', '/compiled/Hlsl/')
|
|
|
|
|
|
|
|
|
|
shader_file = matname + '_' + output_rpass + '.' + ext + output_ext
|
|
|
|
|
shader_path = lnx.utils.get_fp() + '/' + output_rel_path + '/' + shader_file
|
|
|
|
|
assets.add_shader(shader_path)
|
|
|
|
|
shader_paths.append(shader_path)
|
|
|
|
|
|
|
|
|
|
if os.path.isfile(shader_path) and keep_cache:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
content = shader.get()
|
|
|
|
|
|
|
|
|
|
if shader.noprocessing:
|
|
|
|
|
written = lnx.utils.write_if_changed(shader_path, content)
|
|
|
|
|
if written:
|
|
|
|
|
cwd = os.getcwd()
|
|
|
|
|
os.chdir(lnx.utils.get_fp() + '/' + output_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'
|
|
|
|
|
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)
|
|
|
|
|
else:
|
|
|
|
|
write_tasks.append((shader_path, content))
|
|
|
|
|
|
|
|
|
|
if write_tasks:
|
|
|
|
|
with ThreadPoolExecutor(max_workers=min(5, len(write_tasks))) as pool:
|
|
|
|
|
list(pool.map(lambda t: lnx.utils.write_if_changed(t[0], t[1]), write_tasks))
|
|
|
|
|
|
|
|
|
|
return shader_paths
|
2025-01-22 16:18:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-02-24 17:35:26 -08:00
|
|
|
validation_issues = shader.validate()
|
|
|
|
|
if validation_issues:
|
|
|
|
|
for issue in validation_issues:
|
|
|
|
|
log.warn(f"Shader validation issue in {matname}_{rpass}.{ext}: {issue}. ")
|
|
|
|
|
|
2025-01-22 16:18:30 +01:00
|
|
|
# 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/'
|
2026-08-27 15:14:57 -07:00
|
|
|
os.makedirs(hlsl_dir, exist_ok=True)
|
2025-01-22 16:18:30 +01:00
|
|
|
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:
|
2026-04-27 19:21:50 -07:00
|
|
|
written = lnx.utils.write_if_changed(shader_path, shader.get())
|
2025-01-22 16:18:30 +01:00
|
|
|
|
2026-04-27 19:21:50 -07:00
|
|
|
if written and shader.noprocessing:
|
2025-01-22 16:18:30 +01:00
|
|
|
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
|
2026-06-24 20:33:46 -07:00
|
|
|
if inst != 'Off' or (mat.lnx_particle_flag and lnx.utils.get_rp().lnx_particles == 'GPU'):
|
2025-01-22 16:18:30 +01:00
|
|
|
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'})
|
2025-06-01 23:46:05 +00:00
|
|
|
if 'Scale' in inst:
|
2025-01-22 16:18:30 +01:00
|
|
|
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
|