Files
LNXSDK/leenkx/blender/lnx/material/make.py
2026-08-07 02:04:01 -07:00

330 lines
16 KiB
Python

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)) == "<class 'bpy_prop_array'>":
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': []}
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 = 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 = 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})
# extended BRDF parameters as bind_constants
if rpdat.rp_renderer == 'Deferred':
feats = mat_state.features
import re as _re_mix
_mix_re = _re_mix.compile(r'^\s*([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)\s*\*\s*\w+\s*([+-])\s*([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)\s*\*\s*\w+\s*$')
_term_re = _re_mix.compile(r'([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)\s*\*\s*[^\s*]+(?:\s*\*\s*[^\s*]+)*')
_term_pair_re = _re_mix.compile(r'([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)\s*\*\s*([^\s*]+(?:\s*\*\s*[^\s*]+)*)')
def _parse_terms(val_str):
val_str = str(val_str).strip()
pairs = _term_pair_re.findall(val_str)
return [(float(c), v.replace(' ', '')) for c, v in pairs]
def eval_filtered_expr(val_str, filter_str):
if val_str is None or filter_str is None:
return None
filter_terms = _parse_terms(filter_str)
if len(filter_terms) < 2:
return None
filter_map = {}
for coeff, var_prod in filter_terms:
filter_map[var_prod] = coeff
val_terms = _parse_terms(val_str)
if len(val_terms) < 2:
return None
filtered_coeffs = []
for coeff, var_prod in val_terms:
fc = filter_map.get(var_prod)
if fc is not None and fc != 0.0:
filtered_coeffs.append(coeff)
if not filtered_coeffs:
return None
if all(c == filtered_coeffs[0] for c in filtered_coeffs):
return filtered_coeffs[0]
return None
def eval_mix_expr(val_str):
if val_str is None:
return None
val_str = str(val_str).strip()
try:
return float(val_str)
except (ValueError, TypeError):
pass
m = _mix_re.match(val_str)
if m:
a = float(m.group(1))
op = m.group(2)
b = float(m.group(3))
if op == '+' and a == b:
return a
if op == '-' and a == b:
return 0.0
terms = _term_re.findall(val_str)
if terms and len(terms) >= 2:
coeffs = [float(t) for t in terms]
nonzero = [c for c in coeffs if c != 0.0]
if not nonzero:
return 0.0
if all(c == nonzero[0] for c in nonzero):
return nonzero[0]
return None
def feat_is_nonzero(val_str):
v = eval_mix_expr(val_str)
if v is None:
return True
return v != 0.0
def try_float(val_str, default=1.0):
v = eval_mix_expr(val_str)
return v if v is not None else default
def is_constant(val_str):
return eval_mix_expr(val_str) is not None
warned_nonconst = set()
def ext_bc(name, val_str, default):
const = is_constant(val_str)
if not const and name not in warned_nonconst:
warned_nonconst.add(name)
log.warn(f'material "{material.name}": deferred ext BRDF param "{name}" has non-constant expression "{val_str}", using default {default} - per-pixel texture-driven params require deferred texturing (Phase 4)')
c['bind_constants'].append({'name': name, 'floatValue': try_float(val_str, default), 'is_constant': const})
has_ext_brdf = False
if feat_is_nonzero(feats.get('clearcoat', '0.0')):
ext_bc('clearcoat', feats.get('clearcoat', '1.0'), 1.0)
ext_bc('clearcoatRough', feats.get('clearcoatRough', '0.03'), 0.03)
ext_bc('coatIOR', feats.get('coatIOR', '1.5'), 1.5)
ext_bc('coatTintR', feats.get('coatTintR', '1.0'), 1.0)
ext_bc('coatTintG', feats.get('coatTintG', '1.0'), 1.0)
ext_bc('coatTintB', feats.get('coatTintB', '1.0'), 1.0)
if '_ClearCoat' not in wrd.world_defs:
wrd.world_defs += '_ClearCoat'
has_ext_brdf = True
if feat_is_nonzero(feats.get('sheen', '0.0')):
ext_bc('sheen', feats.get('sheen', '1.0'), 1.0)
ext_bc('sheenRough', feats.get('sheenRough', '0.5'), 0.5)
ext_bc('sheenTintR', feats.get('sheenTintR', '1.0'), 1.0)
ext_bc('sheenTintG', feats.get('sheenTintG', '1.0'), 1.0)
ext_bc('sheenTintB', feats.get('sheenTintB', '1.0'), 1.0)
if '_Sheen' not in wrd.world_defs:
wrd.world_defs += '_Sheen'
has_ext_brdf = True
if feat_is_nonzero(feats.get('subsurface', '0.0')) and rpdat.rp_sss_state != 'Off':
ext_bc('subsurface', feats.get('subsurface', '1.0'), 1.0)
ext_bc('subsurfaceAnisotropy', feats.get('subsurfaceAnisotropy', '0.0'), 0.0)
ext_bc('subsurfaceScale', feats.get('subsurfaceScale', '0.05'), 0.05)
ext_bc('subsurfaceRadiusR', feats.get('subsurfaceRadiusR', '1.0'), 1.0)
ext_bc('subsurfaceRadiusG', feats.get('subsurfaceRadiusG', '0.2'), 0.2)
ext_bc('subsurfaceRadiusB', feats.get('subsurfaceRadiusB', '0.1'), 0.1)
ext_bc('subsurfaceColorR', feats.get('subsurfaceColorR', '0.8'), 0.8)
ext_bc('subsurfaceColorG', feats.get('subsurfaceColorG', '0.8'), 0.8)
ext_bc('subsurfaceColorB', feats.get('subsurfaceColorB', '0.8'), 0.8)
if '_SSS' not in wrd.world_defs:
wrd.world_defs += '_SSS'
has_ext_brdf = True
sss_used = True
if feat_is_nonzero(feats.get('anisotropy', '0.0')):
ext_bc('anisotropy', feats.get('anisotropy', '1.0'), 1.0)
ext_bc('anisoRot', feats.get('anisoRot', '0.0'), 0.0)
if '_Anisotropy' not in wrd.world_defs:
wrd.world_defs += '_Anisotropy'
has_ext_brdf = True
if feat_is_nonzero(feats.get('transmission', '0.0')):
transm_str = feats.get('transmission', '1.0')
ext_bc('transmission', transm_str, 1.0)
ext_bc('transmissionRough', feats.get('transmissionRough', '0.0'), 0.0)
ior_str = feats.get('ior', '1.45')
if not is_constant(ior_str):
filtered = eval_filtered_expr(ior_str, transm_str)
if filtered is not None:
ior_str = str(filtered)
ext_bc('ior', ior_str, 1.45)
ext_bc('thinWall', 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'
if has_ext_brdf:
ext_bc('specularTintR', feats.get('specularTintR', '1.0'), 1.0)
ext_bc('specularTintG', feats.get('specularTintG', '1.0'), 1.0)
ext_bc('specularTintB', feats.get('specularTintB', '1.0'), 1.0)
# 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')
has_ext_brdf = False
has_matid = any(bc.get('name') == 'materialID' for bc in c['bind_constants'] if isinstance(bc, dict))
if not has_matid:
c['bind_constants'].append({'name': 'materialID', 'intValue': 0})
# 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