Group Nodes, Shaders, Textures, Displacement

This commit is contained in:
2026-09-20 11:15:14 -07:00
parent f6d5f142e9
commit fac01a2849
14 changed files with 500 additions and 255 deletions

View File

@ -288,10 +288,16 @@ def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types.
wrd.world_defs += '_EmissionShaded'
lnx.assets.add_khafile_def('rp_gbuffer_emission')
disp_geom, disp_bump = disp_mode(node)
disp_active = parse_displacement and node.inputs[2].is_linked and (disp_geom != 'off' or disp_bump)
# Surface
if parse_surface or parse_opacity:
state.parents = []
state.parsed = set()
state.parsing = set()
state.node_uids = {}
state.node_uids_used = set()
state.normal_parsed = False
curshader = state.frag
state.curshader = curshader
@ -305,6 +311,29 @@ def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types.
out_opacity = outs[5]
out_ior = outs[6]
out_emission_col = outs[7]
if parse_surface and not state.basecol_only and disp_active and disp_bump:
state.dxdy_scale = '0.1'
out_disp = parse_displacement_input(node.inputs[2])
state.current_pass = ParserPass.DX_SCREEN_SPACE
out_disp_dx = parse_displacement_input(node.inputs[2])
state.current_pass = ParserPass.DY_SCREEN_SPACE
out_disp_dy = parse_displacement_input(node.inputs[2])
state.current_pass = ParserPass.REGULAR
state.dxdy_scale = '1.0'
curshader.write('vec3 disp = {0};'.format(out_disp))
curshader.write('vec3 disp_dx = {0};'.format(out_disp_dx))
curshader.write('vec3 disp_dy = {0};'.format(out_disp_dy))
curshader.write('float bump_h = dot(disp, n);')
curshader.write('vec2 bump_dHd = vec2(dot(disp_dx, n + {0}), dot(disp_dy, n + {1})) - bump_h;'.format(dfdx_fine('n'), dfdy_fine('n')))
curshader.write('vec3 bump_dPdx = {0};'.format(dfdx_fine('wposition')))
curshader.write('vec3 bump_dPdy = {0};'.format(dfdy_fine('wposition')))
curshader.write('vec3 bump_Rx = cross(bump_dPdy, n);')
curshader.write('vec3 bump_Ry = cross(n, bump_dPdx);')
curshader.write('float bump_det = dot(bump_dPdx, bump_Rx);')
curshader.write('vec3 bump_surfgrad = bump_dHd.x * bump_Rx + bump_dHd.y * bump_Ry;')
curshader.write('float bump_facing = gl_FrontFacing ? 1.0 : -1.0;')
curshader.write('n = normalize(0.1 * abs(bump_det) * n - bump_facing * sign(bump_det) * bump_surfgrad);')
if parse_surface:
curshader.write(f'basecol = {out_basecol};')
curshader.write(f'roughness = {out_roughness};')
@ -347,13 +376,14 @@ def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types.
# Volume
# parse_volume_input(node.inputs[1])
# Displacement
if parse_displacement and disp_enabled() and node.inputs[2].is_linked:
if disp_active and disp_geom != 'off':
state.parents = []
state.parsed = set()
state.parsing = set()
state.node_uids = {}
state.node_uids_used = set()
state.normal_parsed = False
rpdat = lnx.utils.get_rp()
if rpdat.lnx_rp_displacement == 'Tessellation' and state.tese is not None:
if disp_geom == 'tessellation' and state.tese is not None:
state.curshader = state.tese
else:
state.curshader = state.vert
@ -361,9 +391,12 @@ def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types.
state.curshader.write('vec3 disp = {0};'.format(out_disp))
if custom_particle_node is not None:
if not (parse_displacement and disp_enabled() and node.inputs[2].is_linked):
if not (disp_active and disp_geom != 'off'):
state.parents = []
state.parsed = set()
state.parsing = set()
state.node_uids = {}
state.node_uids_used = set()
state.normal_parsed = False
state.curshader = state.vert
@ -511,6 +544,9 @@ def parse_displacement_input(inp):
l = inp.links[0]
if l.from_node.type == 'REROUTE':
return parse_displacement_input(l.from_node.inputs[0])
if l.from_socket.type in ('VALUE', 'INT', 'BOOLEAN'):
nor = 'n' if state.curshader.shader_type == 'frag' else 'wnormal'
return '{0} * vec3({1})'.format(nor, parse_value_input(inp))
return parse_vector_input(inp)
else:
return None
@ -527,7 +563,7 @@ def parse_vector_input(inp: bpy.types.NodeSocket) -> vec3str:
st = link.from_socket.type
if st in ('RGB', 'RGBA', 'VECTOR'):
return res_var
elif st in ('VALUE', 'INT'):
elif st in ('VALUE', 'INT', 'BOOLEAN'):
return f'vec3({res_var})'
else:
log.warn(f'Node tree "{tree_name()}": socket "{link.from_socket.name}" of node "{link.from_node.name}" cannot be connected to a vector-like socket')
@ -572,6 +608,7 @@ def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str:
'HUE_SAT',
'INVERT',
'MIX',
'MIX_RGB',
'BLACKBODY',
'VALTORGB',
'CURVE_VEC',
@ -590,6 +627,8 @@ def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str:
'TANGENT',
'TEX_COORD',
'UVMAP',
'VOLUME_INFO',
'BEVEL',
'BUMP',
'MAPPING',
'NORMAL',
@ -658,7 +697,8 @@ def parse_normal_map_color_input(inp, strength_input=None, space='TANGENT'):
frag.write(f'vec3 texn = ({color_val}) * 2.0 - 1.0;')
if strength != '1.0':
frag.write(f'texn.xy *= {strength};')
frag.write('n = normalize(TBN * texn);')
frag.write('vec3 bitan = cross(n, TBN[0]) * sign(dot(cross(n, TBN[0]), TBN[1]));')
frag.write('n = normalize(mat3(TBN[0], bitan, n) * texn);')
state.con.add_elem('tang', 'short4norm')
elif space in ['OBJECT', 'BLENDER_OBJECT']:
@ -691,10 +731,8 @@ def parse_value_input(inp: bpy.types.NodeSocket) -> floatstr:
if socket_type in ('RGB', 'RGBA', 'VECTOR'):
# RGB to BW
return rgb_to_bw(res_var)
elif socket_type in ('VALUE', 'INT'):
elif socket_type in ('VALUE', 'INT', 'BOOLEAN'):
return res_var
elif socket_type == 'BOOLEAN':
return f'({res_var} ? 1.0 : 0.0)'
else:
log.warn(f'Node tree "{tree_name()}": socket "{link.from_socket.name}" of node "{link.from_node.name}" cannot be connected to a scalar value socket')
return '0.0'
@ -745,6 +783,7 @@ def parse_value(node, socket):
'SEPXYZ',
'VECT_MATH',
'MAP_RANGE',
'VOLUME_INFO',
)
if node.type in supported_node_types:
@ -811,7 +850,7 @@ def is_parsed(node_store_name: str):
def res_var_name(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str:
"""Return the name of the variable that stores the parsed result
from the given node and socket."""
name = node_name(node.name) + '_' + safesrc(socket.name) + '_res'
name = node_uid(node) + '_' + safesrc(socket.name) + '_res'
if '__' in name: # Consecutive _ are reserved
name = name.replace('_', '_x')
return name
@ -821,36 +860,47 @@ def write_result(link: bpy.types.NodeLink) -> Optional[str]:
"""Write the parsed result of the given node link to the shader."""
res_var = res_var_name(link.from_node, link.from_socket)
need_dxdy_offset = node_need_reevaluation_for_screenspace_derivative(link.from_node)
need_dxdy_offset = node_need_reevaluation_for_screenspace_derivative(link.from_node, link.from_socket)
if need_dxdy_offset:
res_var += state.get_parser_pass_suffix()
# Unparsed node
if not is_parsed(res_var):
state.parsed.add(res_var)
state.parsing.add(res_var)
st = link.from_socket.type
if st in ('RGB', 'RGBA', 'VECTOR'):
res = parse_vector(link.from_node, link.from_socket)
if res is None:
log.error(f'{link.from_node.name} returned `None` while parsing!')
return None
state.curshader.write(f'vec3 {res_var} = {res};')
try:
if st in ('RGB', 'RGBA', 'VECTOR'):
res = parse_vector(link.from_node, link.from_socket)
if res is None:
log.error(f'{link.from_node.name} returned `None` while parsing!')
return None
state.curshader.write(f'vec3 {res_var} = {res};')
elif st in ('VALUE', 'INT', 'BOOLEAN'):
res = parse_value(link.from_node, link.from_socket)
if res is None:
log.error(f'{link.from_node.name} returned `None` while parsing!')
return None
if link.from_node.type == "VALUE" and not link.from_node.lnx_material_param:
state.curshader.add_const('float', res_var, res)
else:
state.curshader.write(f'float {res_var} = {res};')
elif st == 'VALUE':
res = parse_value(link.from_node, link.from_socket)
if res is None:
log.error(f'{link.from_node.name} returned `None` while parsing!')
return None
if link.from_node.type == "VALUE" and not link.from_node.lnx_material_param:
state.curshader.add_const('float', res_var, res)
else:
state.curshader.write(f'float {res_var} = {res};')
state.curshader.write(f'float {res_var} = 0.0;')
finally:
state.parsing.discard(res_var)
if state.dxdy_varying_input_value:
state.curshader.write(f'{res_var} = {apply_screenspace_derivative_offset_if_required(res_var)};')
state.dxdy_varying_input_value = False
elif res_var in state.parsing:
log.warn(f'Node tree "{tree_name()}": cyclic dependency on node "{link.from_node.name}" ({res_var}); a node cannot depend on itself. Check the node group for a feedback loop.')
return 'vec3(0.0)' if link.from_socket.type in ('RGB', 'RGBA', 'VECTOR') else '0.0'
# Normal map already parsed, return
elif link.from_node.type == 'NORMAL_MAP':
return None
@ -877,7 +927,7 @@ def to_uniform(inp: bpy.types.NodeSocket):
def store_var_name(node: bpy.types.Node) -> str:
name = node_name(node.name)
name = node_uid(node)
if name[-1] == "_":
return name + '_x_store' # Prevent consecutive __
return name + '_store'
@ -919,7 +969,7 @@ def texture_store(node, tex, tex_name, to_linear=False, unpremultiply=False, tex
nor = 'TBN[2]'
else:
nor = 'n'
blend = getattr(node, 'projection_blend', 0.0)
blend = node.projection_blend
curshader.write('vec4 {0} = boxProjection({1}, {2}, {3}, clamp({4}, 0.0, 1.0));'.format(tex_store, tex_name, nor, uv_name, blend))
elif spherical:
if not curshader.has_include('std/mapping.glsl'):
@ -951,17 +1001,48 @@ def apply_screenspace_derivative_offset_if_required(coords: str) -> str:
# Derivative functions are only available in fragment shaders
if state.curshader.shader_type == 'frag':
if state.current_pass == ParserPass.DX_SCREEN_SPACE:
coords = f'({coords}) + {dfdx_fine(coords)}'
coords = f'({coords}) + {dfdx_fine(coords)} * ({state.dxdy_scale})'
elif state.current_pass == ParserPass.DY_SCREEN_SPACE:
coords = f'({coords}) + {dfdy_fine(coords)}'
coords = f'({coords}) + {dfdy_fine(coords)} * ({state.dxdy_scale})'
return '(' + coords + ')'
def node_need_reevaluation_for_screenspace_derivative(node: bpy.types.Node) -> bool:
def node_need_reevaluation_for_screenspace_derivative(
node: bpy.types.Node,
socket: Optional[bpy.types.NodeSocket] = None,
parents: Tuple[bpy.types.Node, ...] = (),
) -> bool:
if state.current_pass not in (ParserPass.DX_SCREEN_SPACE, ParserPass.DY_SCREEN_SPACE):
return False
if node.type == 'GROUP':
output_node = node_by_type(node.node_tree.nodes, 'GROUP_OUTPUT')
if output_node is None:
return False
if socket is not None:
index = socket_index(node, socket)
inputs = [output_node.inputs[index]] if index is not None and index < len(output_node.inputs) else []
else:
inputs = output_node.inputs
for inp in inputs:
c_node, c_socket = lnx.node_utils.input_get_connected_node(inp)
if c_node is not None and node_need_reevaluation_for_screenspace_derivative(c_node, c_socket, parents + (node,)):
return True
return False
if node.type == 'GROUP_INPUT':
if socket is None or len(parents) == 0:
return False
index = socket_index(node, socket)
parent = parents[-1]
if index is None or index >= len(parent.inputs):
return False
c_node, c_socket = lnx.node_utils.input_get_connected_node(parent.inputs[index])
if c_node is None:
return False
return node_need_reevaluation_for_screenspace_derivative(c_node, c_socket, parents[:-1])
should_compute_offset = node_meta.get_node_meta(node).compute_dxdy_variants
if should_compute_offset == node_meta.ComputeDXDYVariant.ALWAYS:
@ -971,11 +1052,11 @@ def node_need_reevaluation_for_screenspace_derivative(node: bpy.types.Node) -> b
# ComputeDXDYVariant.DYNAMIC
for inp in node.inputs:
c_node, _ = lnx.node_utils.input_get_connected_node(inp)
c_node, c_socket = lnx.node_utils.input_get_connected_node(inp)
if c_node is None:
continue
if node_need_reevaluation_for_screenspace_derivative(c_node):
if node_need_reevaluation_for_screenspace_derivative(c_node, c_socket, parents):
return True
return False
@ -1077,6 +1158,22 @@ def node_name(s: str) -> str:
s = s.replace('_', '_x')
return s
def node_uid(node: bpy.types.Node) -> str:
ctx = (tuple(p.as_pointer() for p in state.parents), state.curshader.write_textures > 0)
key = (node.as_pointer(), ctx)
name = state.node_uids.get(key)
if name is None:
base = node_name(node.name)
name = base
i = 1
while name in state.node_uids_used:
i += 1
name = f'{base}{i}' if base.endswith('_') else f'{base}_{i}'
state.node_uids[key] = name
state.node_uids_used.add(name)
return name
##
@ -1256,6 +1353,32 @@ def safesrc(name):
def disp_enabled():
return lnx.utils.disp_enabled(lnx.make_state.target)
def disp_mode(output_node):
inp = output_node.inputs[2]
if not inp.is_linked:
return ('off', False)
l = inp.links[0]
if l.from_node.type == 'GROUP' and l.from_node.node_tree.name.startswith('Leenkx PBR') and \
not l.from_node.inputs[7].is_linked:
return ('off', False)
mech = lnx.utils.get_rp().lnx_rp_displacement
if bpy.app.version >= (4, 1, 0):
method = mat_state.material.displacement_method
else:
method = getattr(mat_state.material.cycles, 'displacement_method', 'BUMP')
if mech == 'Off':
return ('off', False)
if mech == 'Bump':
return ('off', True)
if mech == 'Tessellation' and not disp_enabled():
log.warn('Tessellation not available on ' + lnx.make_state.target)
return ('off', True)
geom = 'tessellation' if mech == 'Tessellation' else 'vertex'
if method == 'BUMP':
return (geom, False)
return (geom, method in ('DISPLACEMENT', 'BOTH'))
def assets_add(path):
lnx.assets.add(path)