forked from LeenkxTeam/LNXSDK
Group Nodes, Shaders, Textures, Displacement
This commit is contained in:
@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user