Extend BRDF

This commit is contained in:
2026-07-27 12:10:11 -07:00
parent 0b4184ccc2
commit 7aeebf2008
24 changed files with 878 additions and 3 deletions

View File

@ -188,6 +188,64 @@ def parse(nodes, con: ShaderContext,
# Make sure that individual functions in this module aren't called with an incorrect/old parser state, set it to
# None so that it will raise exceptions when not set
# store extended BRDF feature flags before destroying parser state
import re as _re
def _extract_vec3(s):
m = _re.match(r'vec3\s*\(\s*([^,\s\)]+)\s*\)', s)
if m and ',' not in m.group(1):
v = m.group(1)
return v, v, v
m = _re.match(r'vec3\s*\(\s*([^,\s]+)\s*,\s*([^,\s]+)\s*,\s*([^,\s\)]+)', s)
if m:
return m.group(1), m.group(2), m.group(3)
return '1.0', '1.0', '1.0'
def _try_float(val_str, default=0.0):
try:
return float(val_str)
except (ValueError, TypeError):
return default
_sss_r, _sss_g, _sss_b = _extract_vec3(state.out_subsurface_radius)
_sss_scale = state.out_subsurface_scale
_sss_scale_f = _try_float(_sss_scale, 0.05)
if _sss_scale_f != 1.0:
_sss_r = str(_try_float(_sss_r, 1.0) * _sss_scale_f)
_sss_g = str(_try_float(_sss_g, 0.2) * _sss_scale_f)
_sss_b = str(_try_float(_sss_b, 0.1) * _sss_scale_f)
_sheen_tint_r, _sheen_tint_g, _sheen_tint_b = _extract_vec3(state.out_sheen_tint)
_coat_tint_r, _coat_tint_g, _coat_tint_b = _extract_vec3(state.out_coat_tint)
_sss_color_r, _sss_color_g, _sss_color_b = _extract_vec3(state.out_subsurface_color)
mat_state.features = {
'clearcoat': state.out_clearcoat,
'clearcoatRough': state.out_clearcoat_rough,
'coatIOR': state.out_coat_ior,
'coatTintR': _coat_tint_r,
'coatTintG': _coat_tint_g,
'coatTintB': _coat_tint_b,
'sheen': state.out_sheen,
'sheenRough': state.out_sheen_rough,
'sheenTintR': _sheen_tint_r,
'sheenTintG': _sheen_tint_g,
'sheenTintB': _sheen_tint_b,
'subsurface': state.out_subsurface,
'subsurfaceAnisotropy': state.out_subsurface_anisotropy,
'subsurfaceRadiusR': _sss_r,
'subsurfaceRadiusG': _sss_g,
'subsurfaceRadiusB': _sss_b,
'subsurfaceColorR': _sss_color_r,
'subsurfaceColorG': _sss_color_g,
'subsurfaceColorB': _sss_color_b,
'anisotropy': state.out_anisotropy,
'anisoRot': state.out_aniso_rot,
'transmission': state.out_transmission,
'transmissionRough': state.out_transmission_rough,
'ior': state.out_ior,
'thinWall': state.out_thin_wall,
}
state = None
@ -250,6 +308,10 @@ def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types.
curshader.write(f'transmissionRough = {outs[20]};')
curshader.write(f'thinWall = {outs[21]};')
curshader.write(f'tangent = {outs[22]};')
curshader.write(f'subsurfaceScale = {outs[23]};')
curshader.write(f'subsurfaceAnisotropy = {outs[24]};')
curshader.write(f'coatIOR = {outs[25]};')
curshader.write(f'coatTint = {outs[26]};')
if mat_state.emission_type == mat_state.EmissionType.SHADELESS:
if '_EmissionShadeless' not in wrd.world_defs:

View File

@ -55,6 +55,8 @@ else:
TRANSMISSION_ROUGHNESS = 'Transmission Roughness'
TANGENT = 'Tangent'
THIN_WALL = 'Thin Wall'
SUBSURFACE_SCALE = 'Subsurface Scale'
SUBSURFACE_ANISOTROPY = 'Subsurface Anisotropy'
def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, state: ParserState) -> None:
@ -111,6 +113,10 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket,
state.out_transmission_rough = fm.format(o1[20], o2[20], fv[0], fv[1])
state.out_thin_wall = fm.format(o1[21], o2[21], fv[0], fv[1])
state.out_tangent = fm.format(o1[22], o2[22], fv[0], fv[1])
state.out_subsurface_scale = fm.format(o1[23], o2[23], fv[0], fv[1])
state.out_subsurface_anisotropy = fm.format(o1[24], o2[24], fv[0], fv[1])
state.out_coat_ior = fm.format(o1[25], o2[25], fv[0], fv[1])
state.out_coat_tint = fm.format(o1[26], o2[26], fv[0], fv[1])
mat_state.emission_type = mat_state.EmissionType.get_effective_combination(ek1, ek2)
if state.parse_opacity:
fm = '{0} * {3} + {1} * {2}'
@ -155,6 +161,10 @@ def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket,
state.out_transmission_rough = am.format(o1[20], o2[20])
state.out_thin_wall = am.format(o1[21], o2[21])
state.out_tangent = am.format(o1[22], o2[22])
state.out_subsurface_scale = am.format(o1[23], o2[23])
state.out_subsurface_anisotropy = am.format(o1[24], o2[24])
state.out_coat_ior = am.format(o1[25], o2[25])
state.out_coat_tint = am.format(o1[26], o2[26])
mat_state.emission_type = mat_state.EmissionType.get_effective_combination(ek1, ek2)
if state.parse_opacity:
am = '{0} * 0.5 + {1} * 0.5'
@ -254,6 +264,14 @@ if bpy.app.version >= (4, 0, 0):
if sss_radius_input is not None:
state.out_subsurface_radius = c.parse_vector_input(sss_radius_input)
sss_scale_input = node.inputs.get(SUBSURFACE_SCALE)
if sss_scale_input is not None:
state.out_subsurface_scale = c.parse_value_input(sss_scale_input)
sss_aniso_input = node.inputs.get(SUBSURFACE_ANISOTROPY)
if sss_aniso_input is not None:
state.out_subsurface_anisotropy = c.parse_value_input(sss_aniso_input)
sss_color_input = node.inputs.get(SUBSURFACE_COLOR)
if sss_color_input is not None:
state.out_subsurface_color = c.parse_vector_input(sss_color_input)
@ -307,6 +325,12 @@ if bpy.app.version >= (4, 0, 0):
coat_rough_socket = node.inputs.get(CLEARCOAT_ROUGHNESS)
if coat_rough_socket is not None:
state.out_clearcoat_rough = c.parse_value_input(coat_rough_socket)
coat_ior_socket = node.inputs.get(COAT_IOR)
if coat_ior_socket is not None:
state.out_coat_ior = c.parse_value_input(coat_ior_socket)
coat_tint_socket = node.inputs.get(COAT_TINT)
if coat_tint_socket is not None:
state.out_coat_tint = c.parse_vector_input(coat_tint_socket)
trans_socket = node.inputs.get(TRANSMISSION)
if trans_socket is not None:

View File

@ -40,6 +40,8 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]],
wrd = bpy.data.worlds['Lnx']
rpdat = lnx.utils.get_rp()
mat_state.features = {}
# Texture caching for material batching
batch_cached_textures = []
@ -120,6 +122,76 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]],
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

View File

@ -111,6 +111,16 @@ def write(vert: shader.Shader, frag: shader.Shader):
frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix')
frag.add_uniform('vec3 eye', '_cameraPosition')
frag.write(', gbufferD, invVP, eye')
if '_ClearCoat' in wrd.world_defs:
frag.write(', clearcoat, clearcoatRough, coatIOR, coatTint')
if '_Sheen' in wrd.world_defs:
frag.write(', sheen, sheenRough, sheenTint')
if '_Anisotropy' in wrd.world_defs:
frag.write(', anisotropy, anisoRot, tangent')
if '_Subsurface' in wrd.world_defs:
frag.write(', subsurface, subsurfaceColor, subsurfaceRadius, subsurfaceAnisotropy')
if '_Transmission' in wrd.world_defs:
frag.write(', transmission, transmissionRough, ior, thinWall')
frag.write(');')
frag.write('}') # for numLights

View File

@ -74,6 +74,8 @@ def make(context_id, rpasses):
con['color_attachments'] = [attachment_format, attachment_format]
if '_gbuffer2' in wrd.world_defs:
con['color_attachments'].append(attachment_format)
if '_Anisotropy' in wrd.world_defs:
con['color_attachments'].append('RGBA64')
con_mesh = mat_state.data.add_context(con)
mat_state.con_mesh = con_mesh
@ -252,7 +254,9 @@ def make_deferred(con_mesh, rpasses):
frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);')
is_shadeless = mat_state.emission_type == mat_state.EmissionType.SHADELESS
if is_shadeless or '_SSS' in wrd.world_defs or '_Hair' in wrd.world_defs:
ext_brdf_defs = ('_ClearCoat', '_Sheen', '_Anisotropy', '_Subsurface', '_Transmission')
has_ext_brdf = any(d in wrd.world_defs for d in ext_brdf_defs)
if is_shadeless or '_SSS' in wrd.world_defs or '_Hair' in wrd.world_defs or has_ext_brdf:
frag.write('uint matid = 0;')
if is_shadeless:
frag.write('matid = 1;')
@ -260,6 +264,9 @@ def make_deferred(con_mesh, rpasses):
if '_SSS' in wrd.world_defs or '_Hair' in wrd.world_defs:
frag.add_uniform('int materialID')
frag.write('if (materialID == 2) matid = 2;')
if has_ext_brdf:
frag.add_uniform('int materialID')
frag.write('if (materialID >= 3) matid = uint(materialID);')
else:
frag.write('const uint matid = 0;')
@ -285,6 +292,14 @@ def make_deferred(con_mesh, rpasses):
if '_SSRefraction' in wrd.world_defs or '_VoxelRefract' in wrd.world_defs:
frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(1.0, 0.0, 0.0, 1.0);')
if '_Anisotropy' in wrd.world_defs:
frag.write('#ifdef _Anisotropy')
if con_mesh.is_elem('tang'):
frag.write('fragColor[GBUF_IDX_3] = vec4(normalize(TBN[0]), 0.0);')
else:
frag.write('fragColor[GBUF_IDX_3] = vec4(0.0, 0.0, 0.0, 0.0);')
frag.write('#endif')
return con_mesh
@ -830,6 +845,16 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False):
frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix')
frag.add_uniform('vec3 eye', '_cameraPosition')
frag.write(', gbufferD, invVP, eye')
if '_ClearCoat' in wrd.world_defs:
frag.write(', clearcoat, clearcoatRough, coatIOR, coatTint')
if '_Sheen' in wrd.world_defs:
frag.write(', sheen, sheenRough, sheenTint')
if '_Anisotropy' in wrd.world_defs:
frag.write(', anisotropy, anisoRot, tangent')
if '_Subsurface' in wrd.world_defs:
frag.write(', subsurface, subsurfaceColor, subsurfaceRadius, subsurfaceAnisotropy')
if '_Transmission' in wrd.world_defs:
frag.write(', transmission, transmissionRough, ior, thinWall')
frag.write(');')
if '_Clusters' in wrd.world_defs:
@ -872,6 +897,10 @@ def _write_material_attribs_default(frag: shader.Shader, parse_opacity: bool):
frag.write('float transmissionRough = 0.0;')
frag.write('float thinWall = 0.0;')
frag.write('vec3 tangent = vec3(0.0);')
frag.write('float subsurfaceScale = 0.05;')
frag.write('float subsurfaceAnisotropy = 0.0;')
frag.write('float coatIOR = 1.5;')
frag.write('vec3 coatTint = vec3(1.0);')
if parse_opacity:
frag.write('float opacity;')
frag.write('float ior = 1.45;')

View File

@ -81,6 +81,25 @@ def make_gi(context_id):
frag.write('float occlusion;') #
frag.write('float specular;') #
frag.write('vec3 emissionCol = vec3(0.0);')
frag.write('vec3 specularTint = vec3(1.0);')
frag.write('float subsurface = 0.0;')
frag.write('vec3 subsurfaceRadius = vec3(0.0);')
frag.write('vec3 subsurfaceColor = vec3(0.8);')
frag.write('float anisotropy = 0.0;')
frag.write('float anisoRot = 0.0;')
frag.write('float sheen = 0.0;')
frag.write('float sheenRough = 0.5;')
frag.write('vec3 sheenTint = vec3(1.0);')
frag.write('float clearcoat = 0.0;')
frag.write('float clearcoatRough = 0.03;')
frag.write('float transmission = 0.0;')
frag.write('float transmissionRough = 0.0;')
frag.write('float thinWall = 0.0;')
frag.write('vec3 tangent = vec3(0.0);')
frag.write('float subsurfaceScale = 0.05;')
frag.write('float subsurfaceAnisotropy = 0.0;')
frag.write('float coatIOR = 1.5;')
frag.write('vec3 coatTint = vec3(1.0);')
blend = mat_state.material.lnx_blending
parse_opacity = blend or mat_utils.is_transluc(mat_state.material)
if parse_opacity:
@ -377,6 +396,16 @@ def make_gi(context_id):
frag.write(', opacity != 1.0')
if '_Spot' in wrd.world_defs:
frag.write(', true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight')
if '_ClearCoat' in wrd.world_defs:
frag.write(', clearcoat, clearcoatRough, coatIOR, coatTint')
if '_Sheen' in wrd.world_defs:
frag.write(', sheen, sheenRough, sheenTint')
if '_Anisotropy' in wrd.world_defs:
frag.write(', anisotropy, anisoRot, tangent')
if '_Subsurface' in wrd.world_defs:
frag.write(', subsurface, subsurfaceColor, subsurfaceRadius, subsurfaceAnisotropy')
if '_Transmission' in wrd.world_defs:
frag.write(', transmission, transmissionRough, ior, thinWall')
frag.write(');')
if '_Clusters' in wrd.world_defs:
@ -458,6 +487,16 @@ def make_gi(context_id):
frag.write('\t, lightsArraySpot[li * 2].xyz') # spotDir
frag.write('\t, vec2(lightsArray[li * 3].w, lightsArray[li * 3 + 1].w)') # scale
frag.write('\t, lightsArraySpot[li * 2 + 1].xyz') # right
if '_ClearCoat' in wrd.world_defs:
frag.write('\t, clearcoat, clearcoatRough, coatIOR, coatTint')
if '_Sheen' in wrd.world_defs:
frag.write('\t, sheen, sheenRough, sheenTint')
if '_Anisotropy' in wrd.world_defs:
frag.write('\t, anisotropy, anisoRot, tangent')
if '_Subsurface' in wrd.world_defs:
frag.write('\t, subsurface, subsurfaceColor, subsurfaceRadius, subsurfaceAnisotropy')
if '_Transmission' in wrd.world_defs:
frag.write('\t, transmission, transmissionRough, ior, thinWall')
frag.write(' );')
frag.write('}')

View File

@ -39,3 +39,5 @@ con_mesh = None # Mesh context
uses_instancing = False # Whether the current material has at least one user with instancing enabled
emission_type = EmissionType.NO_EMISSION
needs_sss = False
features = {} # tracks extended BRDF features used by current material
next_ext_mat_id = 3 # auto assigned materialID for extended BRDF

View File

@ -114,6 +114,10 @@ class ParserState:
self.out_transmission_rough: floatstr = '0.0'
self.out_thin_wall: floatstr = '0.0'
self.out_tangent: vec3str = 'vec3(0.0)'
self.out_subsurface_scale: floatstr = '0.05'
self.out_subsurface_anisotropy: floatstr = '0.0'
self.out_coat_ior: floatstr = '1.5'
self.out_coat_tint: vec3str = 'vec3(1.0)'
def reset_outs(self):
"""Reset the shader output values to their default values."""
@ -140,6 +144,10 @@ class ParserState:
self.out_transmission_rough = '0.0'
self.out_thin_wall = '0.0'
self.out_tangent = 'vec3(0.0)'
self.out_subsurface_scale = '0.05'
self.out_subsurface_anisotropy = '0.0'
self.out_coat_ior = '1.5'
self.out_coat_tint = 'vec3(1.0)'
def get_outs(self) -> Tuple:
"""Return the shader output values as a tuple."""
@ -152,7 +160,9 @@ class ParserState:
self.out_sheen, self.out_sheen_rough, self.out_sheen_tint,
self.out_clearcoat, self.out_clearcoat_rough,
self.out_transmission, self.out_transmission_rough,
self.out_thin_wall, self.out_tangent)
self.out_thin_wall, self.out_tangent,
self.out_subsurface_scale, self.out_subsurface_anisotropy,
self.out_coat_ior, self.out_coat_tint)
def get_parser_pass_suffix(self) -> str: