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' wrd.world_defs += '_EmissionShaded'
lnx.assets.add_khafile_def('rp_gbuffer_emission') 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 # Surface
if parse_surface or parse_opacity: if parse_surface or parse_opacity:
state.parents = [] state.parents = []
state.parsed = set() state.parsed = set()
state.parsing = set()
state.node_uids = {}
state.node_uids_used = set()
state.normal_parsed = False state.normal_parsed = False
curshader = state.frag curshader = state.frag
state.curshader = curshader 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_opacity = outs[5]
out_ior = outs[6] out_ior = outs[6]
out_emission_col = outs[7] 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: if parse_surface:
curshader.write(f'basecol = {out_basecol};') curshader.write(f'basecol = {out_basecol};')
curshader.write(f'roughness = {out_roughness};') curshader.write(f'roughness = {out_roughness};')
@ -347,13 +376,14 @@ def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types.
# Volume # Volume
# parse_volume_input(node.inputs[1]) # parse_volume_input(node.inputs[1])
# Displacement if disp_active and disp_geom != 'off':
if parse_displacement and disp_enabled() and node.inputs[2].is_linked:
state.parents = [] state.parents = []
state.parsed = set() state.parsed = set()
state.parsing = set()
state.node_uids = {}
state.node_uids_used = set()
state.normal_parsed = False state.normal_parsed = False
rpdat = lnx.utils.get_rp() if disp_geom == 'tessellation' and state.tese is not None:
if rpdat.lnx_rp_displacement == 'Tessellation' and state.tese is not None:
state.curshader = state.tese state.curshader = state.tese
else: else:
state.curshader = state.vert 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)) state.curshader.write('vec3 disp = {0};'.format(out_disp))
if custom_particle_node is not None: 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.parents = []
state.parsed = set() state.parsed = set()
state.parsing = set()
state.node_uids = {}
state.node_uids_used = set()
state.normal_parsed = False state.normal_parsed = False
state.curshader = state.vert state.curshader = state.vert
@ -511,6 +544,9 @@ def parse_displacement_input(inp):
l = inp.links[0] l = inp.links[0]
if l.from_node.type == 'REROUTE': if l.from_node.type == 'REROUTE':
return parse_displacement_input(l.from_node.inputs[0]) 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) return parse_vector_input(inp)
else: else:
return None return None
@ -527,7 +563,7 @@ def parse_vector_input(inp: bpy.types.NodeSocket) -> vec3str:
st = link.from_socket.type st = link.from_socket.type
if st in ('RGB', 'RGBA', 'VECTOR'): if st in ('RGB', 'RGBA', 'VECTOR'):
return res_var return res_var
elif st in ('VALUE', 'INT'): elif st in ('VALUE', 'INT', 'BOOLEAN'):
return f'vec3({res_var})' return f'vec3({res_var})'
else: 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') 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', 'HUE_SAT',
'INVERT', 'INVERT',
'MIX', 'MIX',
'MIX_RGB',
'BLACKBODY', 'BLACKBODY',
'VALTORGB', 'VALTORGB',
'CURVE_VEC', 'CURVE_VEC',
@ -590,6 +627,8 @@ def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str:
'TANGENT', 'TANGENT',
'TEX_COORD', 'TEX_COORD',
'UVMAP', 'UVMAP',
'VOLUME_INFO',
'BEVEL',
'BUMP', 'BUMP',
'MAPPING', 'MAPPING',
'NORMAL', '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;') frag.write(f'vec3 texn = ({color_val}) * 2.0 - 1.0;')
if strength != '1.0': if strength != '1.0':
frag.write(f'texn.xy *= {strength};') 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') state.con.add_elem('tang', 'short4norm')
elif space in ['OBJECT', 'BLENDER_OBJECT']: 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'): if socket_type in ('RGB', 'RGBA', 'VECTOR'):
# RGB to BW # RGB to BW
return rgb_to_bw(res_var) return rgb_to_bw(res_var)
elif socket_type in ('VALUE', 'INT'): elif socket_type in ('VALUE', 'INT', 'BOOLEAN'):
return res_var return res_var
elif socket_type == 'BOOLEAN':
return f'({res_var} ? 1.0 : 0.0)'
else: 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') 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' return '0.0'
@ -745,6 +783,7 @@ def parse_value(node, socket):
'SEPXYZ', 'SEPXYZ',
'VECT_MATH', 'VECT_MATH',
'MAP_RANGE', 'MAP_RANGE',
'VOLUME_INFO',
) )
if node.type in supported_node_types: 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: def res_var_name(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str:
"""Return the name of the variable that stores the parsed result """Return the name of the variable that stores the parsed result
from the given node and socket.""" 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 if '__' in name: # Consecutive _ are reserved
name = name.replace('_', '_x') name = name.replace('_', '_x')
return name 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.""" """Write the parsed result of the given node link to the shader."""
res_var = res_var_name(link.from_node, link.from_socket) 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: if need_dxdy_offset:
res_var += state.get_parser_pass_suffix() res_var += state.get_parser_pass_suffix()
# Unparsed node # Unparsed node
if not is_parsed(res_var): if not is_parsed(res_var):
state.parsed.add(res_var) state.parsed.add(res_var)
state.parsing.add(res_var)
st = link.from_socket.type st = link.from_socket.type
if st in ('RGB', 'RGBA', 'VECTOR'): try:
res = parse_vector(link.from_node, link.from_socket) if st in ('RGB', 'RGBA', 'VECTOR'):
if res is None: res = parse_vector(link.from_node, link.from_socket)
log.error(f'{link.from_node.name} returned `None` while parsing!') if res is None:
return None log.error(f'{link.from_node.name} returned `None` while parsing!')
state.curshader.write(f'vec3 {res_var} = {res};') 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: 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: if state.dxdy_varying_input_value:
state.curshader.write(f'{res_var} = {apply_screenspace_derivative_offset_if_required(res_var)};') state.curshader.write(f'{res_var} = {apply_screenspace_derivative_offset_if_required(res_var)};')
state.dxdy_varying_input_value = False 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 # Normal map already parsed, return
elif link.from_node.type == 'NORMAL_MAP': elif link.from_node.type == 'NORMAL_MAP':
return None return None
@ -877,7 +927,7 @@ def to_uniform(inp: bpy.types.NodeSocket):
def store_var_name(node: bpy.types.Node) -> str: def store_var_name(node: bpy.types.Node) -> str:
name = node_name(node.name) name = node_uid(node)
if name[-1] == "_": if name[-1] == "_":
return name + '_x_store' # Prevent consecutive __ return name + '_x_store' # Prevent consecutive __
return name + '_store' return name + '_store'
@ -919,7 +969,7 @@ def texture_store(node, tex, tex_name, to_linear=False, unpremultiply=False, tex
nor = 'TBN[2]' nor = 'TBN[2]'
else: else:
nor = 'n' 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)) 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: elif spherical:
if not curshader.has_include('std/mapping.glsl'): 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 # Derivative functions are only available in fragment shaders
if state.curshader.shader_type == 'frag': if state.curshader.shader_type == 'frag':
if state.current_pass == ParserPass.DX_SCREEN_SPACE: 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: 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 + ')' 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): if state.current_pass not in (ParserPass.DX_SCREEN_SPACE, ParserPass.DY_SCREEN_SPACE):
return False 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 should_compute_offset = node_meta.get_node_meta(node).compute_dxdy_variants
if should_compute_offset == node_meta.ComputeDXDYVariant.ALWAYS: 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 # ComputeDXDYVariant.DYNAMIC
for inp in node.inputs: 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: if c_node is None:
continue 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 True
return False return False
@ -1077,6 +1158,22 @@ def node_name(s: str) -> str:
s = s.replace('_', '_x') s = s.replace('_', '_x')
return s 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(): def disp_enabled():
return lnx.utils.disp_enabled(lnx.make_state.target) 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): def assets_add(path):
lnx.assets.add(path) lnx.assets.add(path)

View File

@ -1966,107 +1966,110 @@ float snoise(vec4 p) {
p = compatible_mod(p, 100000.0) + precision_correction; p = compatible_mod(p, 100000.0) + precision_correction;
return 0.8344 * noise_perlin(p); return 0.8344 * noise_perlin(p);
} }
"""
#define DEFINE_NOISE_FRACTAL(T) \\\ # Fractal noise variants, emitted on demand per coordinate type.
float noise_fbm(T co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) { \\\ # @T@ is replaced by the coordinate type (float/vec2/vec3/vec4).
T p = co; \\\ str_tex_noise_fbm = """float noise_fbm(@T@ co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) {
float fscale = 1.0; \\\ @T@ p = co;
float amp = 1.0; \\\ float fscale = 1.0;
float maxamp = 0.0; \\\ float amp = 1.0;
float sum = 0.0; \\\ float maxamp = 0.0;
for (int i = 0; i <= int(detail); i++) { \\\ float sum = 0.0;
float t = snoise(fscale * p); \\\ for (int i = 0; i <= int(detail); i++) {
sum += t * amp; \\\ float t = snoise(fscale * p);
maxamp += amp; \\\ sum += t * amp;
amp *= roughness; \\\ maxamp += amp;
fscale *= lacunarity; \\\ amp *= roughness;
} \\\ fscale *= lacunarity;
float rmd = detail - floor(detail); \\\ }
if (rmd != 0.0) { \\\ float rmd = detail - floor(detail);
float t = snoise(fscale * p); \\\ if (rmd != 0.0) {
float sum2 = sum + t * amp; \\\ float t = snoise(fscale * p);
return normalize ? mix(0.5 * sum / maxamp + 0.5, 0.5 * sum2 / (maxamp + amp) + 0.5, rmd) : mix(sum, sum2, rmd); \\\ float sum2 = sum + t * amp;
} else { \\\ return normalize ? mix(0.5 * sum / maxamp + 0.5, 0.5 * sum2 / (maxamp + amp) + 0.5, rmd) : mix(sum, sum2, rmd);
return normalize ? 0.5 * sum / maxamp + 0.5 : sum; \\\ } else {
} \\\ return normalize ? 0.5 * sum / maxamp + 0.5 : sum;
} \\\ }
float noise_multi_fractal(T co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) { \\\ }"""
T p = co; \\\
float value = 1.0; \\\
float pwr = 1.0; \\\
for (int i = 0; i <= int(detail); i++) { \\\
value *= (pwr * snoise(p) + 1.0); \\\
pwr *= roughness; \\\
p *= lacunarity; \\\
} \\\
float rmd = detail - floor(detail); \\\
if (rmd != 0.0) { \\\
value *= (rmd * pwr * snoise(p) + 1.0); \\\
} \\\
return value; \\\
} \\\
float noise_hetero_terrain(T co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) { \\\
T p = co; \\\
float pwr = roughness; \\\
float value = offset + snoise(p); \\\
p *= lacunarity; \\\
for (int i = 1; i <= int(detail); i++) { \\\
float increment = (snoise(p) + offset) * pwr * value; \\\
value += increment; \\\
pwr *= roughness; \\\
p *= lacunarity; \\\
} \\\
float rmd = detail - floor(detail); \\\
if (rmd != 0.0) { \\\
float increment = (snoise(p) + offset) * pwr * value; \\\
value += rmd * increment; \\\
} \\\
return value; \\\
} \\\
float noise_hybrid_multi_fractal(T co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) { \\\
T p = co; \\\
float pwr = 1.0; \\\
float value = 0.0; \\\
float weight = 1.0; \\\
for (int i = 0; (weight > 0.001) && (i <= int(detail)); i++) { \\\
if (weight > 1.0) weight = 1.0; \\\
float signal = (snoise(p) + offset) * pwr; \\\
pwr *= roughness; \\\
value += weight * signal; \\\
weight *= gain * signal; \\\
p *= lacunarity; \\\
} \\\
float rmd = detail - floor(detail); \\\
if ((rmd != 0.0) && (weight > 0.001)) { \\\
if (weight > 1.0) weight = 1.0; \\\
float signal = (snoise(p) + offset) * pwr; \\\
value += rmd * weight * signal; \\\
} \\\
return value; \\\
} \\\
float noise_ridged_multi_fractal(T co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) { \\\
T p = co; \\\
float pwr = roughness; \\\
float signal = offset - abs(snoise(p)); \\\
signal *= signal; \\\
float value = signal; \\\
float weight = 1.0; \\\
for (int i = 1; i <= int(detail); i++) { \\\
p *= lacunarity; \\\
weight = clamp(signal * gain, 0.0, 1.0); \\\
signal = offset - abs(snoise(p)); \\\
signal *= signal; \\\
signal *= weight; \\\
value += signal * pwr; \\\
pwr *= roughness; \\\
} \\\
return value; \\\
}
DEFINE_NOISE_FRACTAL(float) str_tex_noise_multi_fractal = """float noise_multi_fractal(@T@ co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) {
DEFINE_NOISE_FRACTAL(vec2) @T@ p = co;
DEFINE_NOISE_FRACTAL(vec3) float value = 1.0;
DEFINE_NOISE_FRACTAL(vec4) float pwr = 1.0;
for (int i = 0; i <= int(detail); i++) {
value *= (pwr * snoise(p) + 1.0);
pwr *= roughness;
p *= lacunarity;
}
float rmd = detail - floor(detail);
if (rmd != 0.0) {
value *= (rmd * pwr * snoise(p) + 1.0);
}
return value;
}"""
str_tex_noise_hetero_terrain = """float noise_hetero_terrain(@T@ co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) {
@T@ p = co;
float pwr = roughness;
float value = offset + snoise(p);
p *= lacunarity;
for (int i = 1; i <= int(detail); i++) {
float increment = (snoise(p) + offset) * pwr * value;
value += increment;
pwr *= roughness;
p *= lacunarity;
}
float rmd = detail - floor(detail);
if (rmd != 0.0) {
float increment = (snoise(p) + offset) * pwr * value;
value += rmd * increment;
}
return value;
}"""
str_tex_noise_hybrid_multi_fractal = """float noise_hybrid_multi_fractal(@T@ co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) {
@T@ p = co;
float pwr = 1.0;
float value = 0.0;
float weight = 1.0;
for (int i = 0; (weight > 0.001) && (i <= int(detail)); i++) {
if (weight > 1.0) weight = 1.0;
float signal = (snoise(p) + offset) * pwr;
pwr *= roughness;
value += weight * signal;
weight *= gain * signal;
p *= lacunarity;
}
float rmd = detail - floor(detail);
if ((rmd != 0.0) && (weight > 0.001)) {
if (weight > 1.0) weight = 1.0;
float signal = (snoise(p) + offset) * pwr;
value += rmd * weight * signal;
}
return value;
}"""
str_tex_noise_ridged_multi_fractal = """float noise_ridged_multi_fractal(@T@ co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) {
@T@ p = co;
float pwr = roughness;
float signal = offset - abs(snoise(p));
signal *= signal;
float value = signal;
float weight = 1.0;
for (int i = 1; i <= int(detail); i++) {
p *= lacunarity;
weight = clamp(signal * gain, 0.0, 1.0);
signal = offset - abs(snoise(p));
signal *= signal;
signal *= weight;
value += signal * pwr;
pwr *= roughness;
}
return value;
}"""
str_tex_noise += """
float random_float_offset(float seed) { return 100.0 + hash_float_to_float(seed) * 100.0; } float random_float_offset(float seed) { return 100.0 + hash_float_to_float(seed) * 100.0; }
vec2 random_vec2_offset(float seed) { return vec2(100.0 + hash_vec2_to_float(vec2(seed, 0.0)) * 100.0, 100.0 + hash_vec2_to_float(vec2(seed, 1.0)) * 100.0); } vec2 random_vec2_offset(float seed) { return vec2(100.0 + hash_vec2_to_float(vec2(seed, 0.0)) * 100.0, 100.0 + hash_vec2_to_float(vec2(seed, 1.0)) * 100.0); }
@ -2193,6 +2196,43 @@ vec3 hue_sat(const vec3 col, const vec4 shift) {
} }
""" """
str_mix_hsv = """
vec3 mix_hue(const float fac, const vec3 col1, const vec3 col2) {
vec3 hsv2 = rgb_to_hsv(col2);
if (hsv2.y != 0.0) {
vec3 hsv = rgb_to_hsv(col1);
hsv.x = hsv2.x;
return mix(col1, hsv_to_rgb(hsv), fac);
}
return col1;
}
vec3 mix_sat(const float fac, const vec3 col1, const vec3 col2) {
vec3 hsv = rgb_to_hsv(col1);
if (hsv.y != 0.0) {
vec3 hsv2 = rgb_to_hsv(col2);
hsv.y = mix(hsv.y, hsv2.y, fac);
return hsv_to_rgb(hsv);
}
return col1;
}
vec3 mix_val(const float fac, const vec3 col1, const vec3 col2) {
vec3 hsv = rgb_to_hsv(col1);
vec3 hsv2 = rgb_to_hsv(col2);
hsv.z = mix(hsv.z, hsv2.z, fac);
return hsv_to_rgb(hsv);
}
vec3 mix_color(const float fac, const vec3 col1, const vec3 col2) {
vec3 hsv2 = rgb_to_hsv(col2);
if (hsv2.y != 0.0) {
vec3 hsv = rgb_to_hsv(col1);
hsv.x = hsv2.x;
hsv.y = hsv2.y;
return mix(col1, hsv_to_rgb(hsv), fac);
}
return col1;
}
"""
# https://twitter.com/Donzanoid/status/903424376707657730 # https://twitter.com/Donzanoid/status/903424376707657730
str_wavelength_to_rgb = """ str_wavelength_to_rgb = """
vec3 wavelength_to_rgb(const float t) { vec3 wavelength_to_rgb(const float t) {

View File

@ -97,59 +97,76 @@ def _parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSo
col1 = c.parse_vector_input(node.inputs[6]) col1 = c.parse_vector_input(node.inputs[6])
col2 = c.parse_vector_input(node.inputs[7]) col2 = c.parse_vector_input(node.inputs[7])
# Store factor in variable for linked factor input fac = _mixrgb_fac(node, state)
if node.inputs[0].is_linked:
fac = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix()
state.curshader.write('float {0} = {1};'.format(fac, c.parse_value_input(node.inputs[0])))
else:
fac = c.parse_value_input(node.inputs[0])
if node.clamp_factor: if node.clamp_factor:
fac = f'clamp({fac}, 0.0, 1.0)' fac = f'clamp({fac}, 0.0, 1.0)'
# TODO: Do not mix if factor is constant 0.0 or 1.0? # TODO: Do not mix if factor is constant 0.0 or 1.0?
blend = node.blend_type out_col = _mixrgb_blend(col1, col2, fac, node.blend_type, state)
if node.clamp_result:
return 'clamp({0}, vec3(0.0), vec3(1.0))'.format(out_col)
return out_col
def _mixrgb_blend(col1: vec3str, col2: vec3str, fac: str, blend: str, state: ParserState) -> vec3str:
if blend == 'MIX': if blend == 'MIX':
out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) return 'mix({0}, {1}, {2})'.format(col1, col2, fac)
elif blend == 'ADD': elif blend == 'ADD':
out_col = 'mix({0}, {0} + {1}, {2})'.format(col1, col2, fac) return 'mix({0}, {0} + {1}, {2})'.format(col1, col2, fac)
elif blend == 'MULTIPLY': elif blend == 'MULTIPLY':
out_col = 'mix({0}, {0} * {1}, {2})'.format(col1, col2, fac) return 'mix({0}, {0} * {1}, {2})'.format(col1, col2, fac)
elif blend == 'SUBTRACT': elif blend == 'SUBTRACT':
out_col = 'mix({0}, {0} - {1}, {2})'.format(col1, col2, fac) return 'mix({0}, {0} - {1}, {2})'.format(col1, col2, fac)
elif blend == 'SCREEN': elif blend == 'SCREEN':
out_col = 'mix({0}, vec3(1.0) - (vec3(1.0) - {0}) * (vec3(1.0) - {1}), {2})'.format(col1, col2, fac) return 'mix({0}, vec3(1.0) - (vec3(1.0) - {0}) * (vec3(1.0) - {1}), {2})'.format(col1, col2, fac)
elif blend == 'DIVIDE': elif blend == 'DIVIDE':
out_col = 'mix({0}, {0} / max({1}, vec3(0.000001)), {2})'.format(col1, col2, fac) return 'mix({0}, {0} / max({1}, vec3(0.000001)), {2} * vec3(notEqual({1}, vec3(0.0))))'.format(col1, col2, fac)
elif blend == 'DIFFERENCE': elif blend == 'DIFFERENCE':
out_col = 'mix({0}, abs({0} - {1}), {2})'.format(col1, col2, fac) return 'mix({0}, abs({0} - {1}), {2})'.format(col1, col2, fac)
elif blend == 'DARKEN': elif blend == 'DARKEN':
out_col = 'mix({0}, min({0}, {1}), {2})'.format(col1, col2, fac) return 'mix({0}, min({0}, {1}), {2})'.format(col1, col2, fac)
elif blend == 'LIGHTEN': elif blend == 'LIGHTEN':
out_col = 'mix({0}, max({0}, {1}), {2})'.format(col1, col2, fac) return 'mix({0}, max({0}, {1}), {2})'.format(col1, col2, fac)
elif blend == 'OVERLAY': elif blend == 'OVERLAY':
overlay = 'vec3({0}.r < 0.5 ? 2.0 * {0}.r * {1}.r : 1.0 - 2.0 * (1.0 - {0}.r) * (1.0 - {1}.r), {0}.g < 0.5 ? 2.0 * {0}.g * {1}.g : 1.0 - 2.0 * (1.0 - {0}.g) * (1.0 - {1}.g), {0}.b < 0.5 ? 2.0 * {0}.b * {1}.b : 1.0 - 2.0 * (1.0 - {0}.b) * (1.0 - {1}.b))'.format(col1, col2) overlay = 'vec3({0}.r < 0.5 ? 2.0 * {0}.r * {1}.r : 1.0 - 2.0 * (1.0 - {0}.r) * (1.0 - {1}.r), {0}.g < 0.5 ? 2.0 * {0}.g * {1}.g : 1.0 - 2.0 * (1.0 - {0}.g) * (1.0 - {1}.g), {0}.b < 0.5 ? 2.0 * {0}.b * {1}.b : 1.0 - 2.0 * (1.0 - {0}.b) * (1.0 - {1}.b))'.format(col1, col2)
out_col = 'mix({0}, {1}, {2})'.format(col1, overlay, fac) return 'mix({0}, {1}, {2})'.format(col1, overlay, fac)
elif blend == 'DODGE': elif blend == 'DODGE':
dodge = '{0} / max(vec3(1.0) - {1}, vec3(0.000001))'.format(col1, col2) return 'clamp({0} / max(vec3(1.0) - {2} * {1}, vec3(0.000001)), vec3(0.0), vec3(1.0))'.format(col1, col2, fac)
out_col = 'mix({0}, {1}, {2})'.format(col1, dodge, fac)
elif blend == 'BURN': elif blend == 'BURN':
burn = 'vec3(1.0) - (vec3(1.0) - {0}) / max({1}, vec3(0.000001))'.format(col1, col2) return 'clamp(vec3(1.0) - (vec3(1.0) - {0}) / max(vec3(1.0 - {2}) + {2} * {1}, vec3(0.000001)), vec3(0.0), vec3(1.0))'.format(col1, col2, fac)
out_col = 'mix({0}, {1}, {2})'.format(col1, burn, fac)
elif blend == 'SOFT_LIGHT': elif blend == 'SOFT_LIGHT':
soft = '(vec3(1.0) - {1}) * {0} * {0} + {1} * (2.0 * {0} * (vec3(1.0) - {0}) + sqrt({0}) * (2.0 * {0} - vec3(1.0)))'.format(col1, col2) soft = '(vec3(1.0) - {1}) * {0} * {0} + {1} * (2.0 * {0} * (vec3(1.0) - {0}) + sqrt({0}) * (2.0 * {0} - vec3(1.0)))'.format(col1, col2)
out_col = 'mix({0}, {1}, {2})'.format(col1, soft, fac) return 'mix({0}, {1}, {2})'.format(col1, soft, fac)
elif blend == 'LINEAR_LIGHT': elif blend == 'LINEAR_LIGHT':
linear = '{0} + 2.0 * {1} - vec3(1.0)'.format(col1, col2) linear = '{0} + 2.0 * {1} - vec3(1.0)'.format(col1, col2)
out_col = 'mix({0}, {1}, {2})'.format(col1, linear, fac) return 'mix({0}, {1}, {2})'.format(col1, linear, fac)
elif blend == 'EXCLUSION':
return 'max(mix({0}, {0} + {1} - 2.0 * {0} * {1}, {2}), vec3(0.0))'.format(col1, col2, fac)
elif blend in ['HUE', 'SATURATION', 'COLOR', 'VALUE']: elif blend in ['HUE', 'SATURATION', 'COLOR', 'VALUE']:
out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) state.curshader.add_function(c_functions.str_hue_sat)
state.curshader.add_function(c_functions.str_mix_hsv)
func = {'HUE': 'mix_hue', 'SATURATION': 'mix_sat', 'COLOR': 'mix_color', 'VALUE': 'mix_val'}[blend]
return '{3}({2}, {0}, {1})'.format(col1, col2, fac, func)
else: else:
log.warn(f'MixRGB node: unsupported blend type {node.blend_type}.') log.warn(f'MixRGB node: unsupported blend type {blend}.')
out_col = col1 return col1
if node.clamp_result:
def _mixrgb_fac(node: bpy.types.Node, state: ParserState) -> str:
if node.inputs[0].is_linked:
fac = c.node_uid(node) + '_fac' + state.get_parser_pass_suffix()
state.curshader.write('float {0} = {1};'.format(fac, c.parse_value_input(node.inputs[0])))
return fac
return c.parse_value_input(node.inputs[0])
def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
col1 = c.parse_vector_input(node.inputs[1])
col2 = c.parse_vector_input(node.inputs[2])
fac = f'clamp({_mixrgb_fac(node, state)}, 0.0, 1.0)'
out_col = _mixrgb_blend(col1, col2, fac, node.blend_type, state)
if node.use_clamp:
return 'clamp({0}, vec3(0.0), vec3(1.0))'.format(out_col) return 'clamp({0}, vec3(0.0), vec3(1.0))'.format(out_col)
return out_col return out_col
@ -158,7 +175,7 @@ def parse_curvergb(node: bpy.types.ShaderNodeRGBCurve, out_socket: bpy.types.Nod
fac = c.parse_value_input(node.inputs[0]) fac = c.parse_value_input(node.inputs[0])
vec = c.parse_vector_input(node.inputs[1]) vec = c.parse_vector_input(node.inputs[1])
curves = node.mapping.curves curves = node.mapping.curves
name = c.node_name(node.name) name = c.node_uid(node)
# mapping.curves[0].points[0].handle_type # mapping.curves[0].points[0].handle_type
return '(sqrt(vec3({0}, {1}, {2}) * vec3({4}, {5}, {6})) * {3})'.format( return '(sqrt(vec3({0}, {1}, {2}) * vec3({4}, {5}, {6})) * {3})'.format(
c.vector_curve(name + '0', vec + '.x', curves[0].points), c.vector_curve(name + '1', vec + '.y', curves[1].points), c.vector_curve(name + '2', vec + '.z', curves[2].points), fac, c.vector_curve(name + '0', vec + '.x', curves[0].points), c.vector_curve(name + '1', vec + '.y', curves[1].points), c.vector_curve(name + '2', vec + '.z', curves[2].points), fac,

View File

@ -89,7 +89,7 @@ def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.Nod
elems = node.color_ramp.elements elems = node.color_ramp.elements
use_color_out = out_socket == node.outputs[0] use_color_out = out_socket == node.outputs[0]
ramp_store = c.node_name(node.name) + '_res' + state.get_parser_pass_suffix() ramp_store = c.node_uid(node) + '_res' + state.get_parser_pass_suffix()
if c.is_parsed(ramp_store): if c.is_parsed(ramp_store):
return f'{ramp_store}.rgb' if use_color_out else f'{ramp_store}.a' return f'{ramp_store}.rgb' if use_color_out else f'{ramp_store}.a'
@ -100,26 +100,26 @@ def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.Nod
state.parsed.add(ramp_store) state.parsed.add(ramp_store)
return f'{ramp_store}.rgb' if use_color_out else f'{ramp_store}.a' return f'{ramp_store}.rgb' if use_color_out else f'{ramp_store}.a'
cols_var = c.node_name(node.name).upper() + '_COLS' cols_var = c.node_uid(node).upper() + '_COLS'
if state.current_pass == ParserPass.REGULAR: if state.current_pass == ParserPass.REGULAR:
cols_entries = ', '.join(f'vec4({e.color[0]}, {e.color[1]}, {e.color[2]}, {e.color[3]})' for e in elems) cols_entries = ', '.join(f'vec4({e.color[0]}, {e.color[1]}, {e.color[2]}, {e.color[3]})' for e in elems)
cols_entries += f', vec4({elems[-1].color[0]}, {elems[-1].color[1]}, {elems[-1].color[2]}, {elems[-1].color[3]})' cols_entries += f', vec4({elems[-1].color[0]}, {elems[-1].color[1]}, {elems[-1].color[2]}, {elems[-1].color[3]})'
state.curshader.add_const("vec4", cols_var, cols_entries, array_size=len(elems) + 1) state.curshader.add_const("vec4", cols_var, cols_entries, array_size=len(elems) + 1)
fac_var = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix() fac_var = c.node_uid(node) + '_fac' + state.get_parser_pass_suffix()
state.curshader.write(f'float {fac_var} = {fac};') state.curshader.write(f'float {fac_var} = {fac};')
index = '0' index = '0'
for i in range(1, len(elems)): for i in range(1, len(elems)):
index += f' + ({fac_var} > {elems[i].position} ? 1 : 0)' index += f' + ({fac_var} > {elems[i].position} ? 1 : 0)'
index_var = c.node_name(node.name) + '_i' + state.get_parser_pass_suffix() index_var = c.node_uid(node) + '_i' + state.get_parser_pass_suffix()
state.curshader.write(f'int {index_var} = {index};') state.curshader.write(f'int {index_var} = {index};')
if interp == 'CONSTANT': if interp == 'CONSTANT':
state.curshader.write(f'vec4 {ramp_store} = {cols_var}[{index_var}];') state.curshader.write(f'vec4 {ramp_store} = {cols_var}[{index_var}];')
else: else:
facs_var = c.node_name(node.name).upper() + '_FACS' facs_var = c.node_uid(node).upper() + '_FACS'
if state.current_pass == ParserPass.REGULAR: if state.current_pass == ParserPass.REGULAR:
facs_entries = ', '.join(str(e.position) for e in elems) facs_entries = ', '.join(str(e.position) for e in elems)
facs_entries += ', 1.0' facs_entries += ', 1.0'
@ -369,7 +369,7 @@ if bpy.app.version > (3, 2, 0):
def parse_sephsv(node: bpy.types.ShaderNodeSeparateHSV, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: def parse_sephsv(node: bpy.types.ShaderNodeSeparateHSV, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr:
state.curshader.add_function(c_functions.str_hue_sat) state.curshader.add_function(c_functions.str_hue_sat)
hsv_var = c.node_name(node.name) + '_hsv' + state.get_parser_pass_suffix() hsv_var = c.node_uid(node) + '_hsv' + state.get_parser_pass_suffix()
if not state.curshader.contains(hsv_var): # Already written if a second output is parsed if not state.curshader.contains(hsv_var): # Already written if a second output is parsed
state.curshader.write(f'const vec3 {hsv_var} = rgb_to_hsv({c.parse_vector_input(node.inputs["Color"])}.rgb);') state.curshader.write(f'const vec3 {hsv_var} = rgb_to_hsv({c.parse_vector_input(node.inputs["Color"])}.rgb);')

View File

@ -109,7 +109,7 @@ def parse_attribute(node: bpy.types.ShaderNodeAttribute, out_socket: bpy.types.N
def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
if node.lnx_material_param: if node.lnx_material_param:
nn = 'param_' + c.node_name(node.name) nn = 'param_' + c.node_uid(node)
v = out_socket.default_value v = out_socket.default_value
value = [float(v[0]), float(v[1]), float(v[2])] value = [float(v[0]), float(v[1]), float(v[2])]
state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}', default_value=value, is_lnx_mat_param=True) state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}', default_value=value, is_lnx_mat_param=True)
@ -163,7 +163,17 @@ def parse_geometry(node: bpy.types.ShaderNodeNewGeometry, out_socket: bpy.types.
# Incoming # Incoming
elif out_socket == node.outputs[4]: elif out_socket == node.outputs[4]:
state.dxdy_varying_input_value = True state.dxdy_varying_input_value = True
return 'vVec' if state.curshader.shader_type == 'frag':
return 'vVec'
state.curshader.add_uniform('vec3 eye', link='_cameraPosition')
if state.curshader.shader_type == 'tese':
return 'normalize(eye - wposition)'
if state.curshader.shader_type == 'vert':
state.curshader.add_uniform('mat4 W', link='_worldMatrix')
return 'normalize(eye - (W * spos).xyz)'
if state.curshader.shader_type == 'tesc':
return 'normalize(eye - wposition[gl_InvocationID])'
return 'normalize(eye - wposition[0])'
# Parametric # Parametric
elif out_socket == node.outputs[5]: elif out_socket == node.outputs[5]:
state.dxdy_varying_input_value = True state.dxdy_varying_input_value = True
@ -450,7 +460,7 @@ def parse_lightpath(node: bpy.types.ShaderNodeLightPath, out_socket: bpy.types.N
def parse_value(node: bpy.types.ShaderNodeValue, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: def parse_value(node: bpy.types.ShaderNodeValue, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr:
if node.lnx_material_param: if node.lnx_material_param:
nn = 'param_' + c.node_name(node.name) nn = 'param_' + c.node_uid(node)
value = node.outputs[0].default_value value = node.outputs[0].default_value
is_lnx_mat_param = True is_lnx_mat_param = True
state.curshader.add_uniform('float {0}'.format(nn), link='{0}'.format(node.name), default_value=value, is_lnx_mat_param=is_lnx_mat_param) state.curshader.add_uniform('float {0}'.format(nn), link='{0}'.format(node.name), default_value=value, is_lnx_mat_param=is_lnx_mat_param)
@ -463,3 +473,9 @@ def parse_wireframe(node: bpy.types.ShaderNodeWireframe, out_socket: bpy.types.N
# node.use_pixel_size # node.use_pixel_size
# size = c.parse_value_input(node.inputs[0]) # size = c.parse_value_input(node.inputs[0])
return '0.0' return '0.0'
def parse_volumeinfo(node: bpy.types.ShaderNodeVolumeInfo, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
if out_socket == node.outputs['Color']:
return 'vec3(0.0)'
return '0.0'

View File

@ -69,12 +69,14 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket,
c.parse_shader_input(node.inputs[2]) c.parse_shader_input(node.inputs[2])
return return
prefix = '' if node.inputs['Fac'].is_linked else 'const ' fac_var = c.node_uid(node) + '_fac' + state.get_parser_pass_suffix()
fac = c.parse_value_input(node.inputs['Fac']) fac_inv_var = c.node_uid(node) + '_fac_inv' + state.get_parser_pass_suffix()
fac_var = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix() if not c.is_parsed(fac_var):
fac_inv_var = c.node_name(node.name) + '_fac_inv' state.parsed.add(fac_var)
state.curshader.write('{0}float {1} = clamp({2}, 0.0, 1.0);'.format(prefix, fac_var, fac)) prefix = '' if node.inputs['Fac'].is_linked else 'const '
state.curshader.write('{0}float {1} = 1.0 - {2};'.format(prefix, fac_inv_var, fac_var)) fac = c.parse_value_input(node.inputs['Fac'])
state.curshader.write('{0}float {1} = clamp({2}, 0.0, 1.0);'.format(prefix, fac_var, fac))
state.curshader.write('{0}float {1} = 1.0 - {2};'.format(prefix, fac_inv_var, fac_var))
mat_state.emission_type = mat_state.EmissionType.NO_EMISSION mat_state.emission_type = mat_state.EmissionType.NO_EMISSION
o1 = c.parse_shader_input(node.inputs[1]) o1 = c.parse_shader_input(node.inputs[1])
@ -435,7 +437,7 @@ if bpy.app.version >= (2, 83, 0):
def parse_bsdfhairprincipled(node: 'bpy.types.ShaderNodeBsdfHairPrincipled', out_socket: NodeSocket, state: ParserState) -> None: def parse_bsdfhairprincipled(node: 'bpy.types.ShaderNodeBsdfHairPrincipled', out_socket: NodeSocket, state: ParserState) -> None:
if state.parse_surface: if state.parse_surface:
c.write_normal(node.inputs['Normal']) c.write_normal(node.inputs['Normal'])
parametrization = getattr(node, 'parametrization', 'COLOR') parametrization = node.parametrization
if parametrization == 'COLOR': if parametrization == 'COLOR':
color_socket = node.inputs.get('Color') color_socket = node.inputs.get('Color')
if color_socket is not None: if color_socket is not None:
@ -457,7 +459,7 @@ if bpy.app.version >= (2, 83, 0):
rough_socket = node.inputs.get('Roughness') rough_socket = node.inputs.get('Roughness')
if rough_socket is not None: if rough_socket is not None:
state.out_roughness = c.parse_value_input(rough_socket) state.out_roughness = c.parse_value_input(rough_socket)
model = getattr(node, 'model', 'CHIANG') model = node.model if bpy.app.version >= (4, 0, 0) else 'CHIANG'
if model == 'CHIANG': if model == 'CHIANG':
radial_rough_socket = node.inputs.get('Radial Roughness') radial_rough_socket = node.inputs.get('Radial Roughness')
if radial_rough_socket is not None: if radial_rough_socket is not None:

View File

@ -136,7 +136,7 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No
else: else:
return f'{tex_store}.a' return f'{tex_store}.a'
tex_name = c.node_name(node.name) tex_name = c.node_uid(node)
tex = c.make_texture_from_image_node(node, tex_name) tex = c.make_texture_from_image_node(node, tex_name)
tex_link = None tex_link = None
tex_default_file = None tex_default_file = None
@ -259,7 +259,7 @@ def parse_tex_magic(node: bpy.types.ShaderNodeTexMagic, out_socket: bpy.types.No
return res return res
if bpy.app.version < (4, 1, 0): if bpy.app.version < (4, 2, 0):
def parse_tex_musgrave(node: bpy.types.ShaderNodeTexMusgrave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: def parse_tex_musgrave(node: bpy.types.ShaderNodeTexMusgrave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
state.curshader.add_function(c_functions.str_tex_musgrave) state.curshader.add_function(c_functions.str_tex_musgrave)
@ -297,52 +297,65 @@ def parse_tex_noise(node: bpy.types.ShaderNodeTexNoise, out_socket: bpy.types.No
gain = c.parse_value_input(node.inputs['Gain']) if 'Gain' in node.inputs else '1.0' gain = c.parse_value_input(node.inputs['Gain']) if 'Gain' in node.inputs else '1.0'
distortion = c.parse_value_input(node.inputs['Distortion']) if 'Distortion' in node.inputs else '0.0' distortion = c.parse_value_input(node.inputs['Distortion']) if 'Distortion' in node.inputs else '0.0'
dimensions = getattr(node, 'noise_dimensions', '3D') dimensions = node.noise_dimensions
noise_type = getattr(node, 'noise_type', 'FBM') noise_type = node.noise_type if bpy.app.version >= (4, 1, 0) else 'FBM'
normalize = 'true' if getattr(node, 'normalize', True) else 'false' normalize = 'true' if bpy.app.version < (4, 0, 0) or node.normalize else 'false'
type_map = { if noise_type == 'MULTIFRACTAL':
'FBM': 'noise_fbm', func_name = 'noise_multi_fractal'
'MULTIFRACTAL': 'noise_multi_fractal', fractal_src = c_functions.str_tex_noise_multi_fractal
'RIDGED_MULTIFRACTAL': 'noise_ridged_multi_fractal', elif noise_type == 'RIDGED_MULTIFRACTAL':
'HYBRID_MULTIFRACTAL': 'noise_hybrid_multi_fractal', func_name = 'noise_ridged_multi_fractal'
'HETERO_TERRAIN': 'noise_hetero_terrain' fractal_src = c_functions.str_tex_noise_ridged_multi_fractal
} elif noise_type == 'HYBRID_MULTIFRACTAL':
func_name = type_map.get(noise_type, 'noise_fbm') func_name = 'noise_hybrid_multi_fractal'
fractal_src = c_functions.str_tex_noise_hybrid_multi_fractal
elif noise_type == 'HETERO_TERRAIN':
func_name = 'noise_hetero_terrain'
fractal_src = c_functions.str_tex_noise_hetero_terrain
else:
func_name = 'noise_fbm'
fractal_src = c_functions.str_tex_noise_fbm
is_color = (out_socket == node.outputs[1]) or (getattr(out_socket, 'name', '') == 'Color') is_color = (out_socket == node.outputs[1]) or (out_socket.name == 'Color')
if dimensions == '1D': if dimensions == '1D':
coord_type = 'float'
p_expr = f"({w}) * ({scale})" p_expr = f"({w}) * ({scale})"
dist_expr = f"({p_expr}) + snoise(({p_expr}) + random_float_offset(0.0)) * ({distortion})" if distortion != '0.0' else p_expr dist_expr = f"({p_expr}) + snoise(({p_expr}) + random_float_offset(0.0)) * ({distortion})" if distortion != '0.0' else p_expr
if is_color: off_a = 'random_float_offset(1.0)'
res = f"vec3({func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_float_offset(1.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_float_offset(2.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}))" off_b = 'random_float_offset(2.0)'
else:
res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})"
elif dimensions == '2D': elif dimensions == '2D':
coord_type = 'vec2'
p_expr = f"({co}).xy * ({scale})" p_expr = f"({co}).xy * ({scale})"
dist_expr = f"({p_expr}) + vec2(snoise(({p_expr}) + random_vec2_offset(0.0)) * ({distortion}), snoise(({p_expr}) + random_vec2_offset(1.0)) * ({distortion}))" if distortion != '0.0' else p_expr dist_expr = f"({p_expr}) + vec2(snoise(({p_expr}) + random_vec2_offset(0.0)) * ({distortion}), snoise(({p_expr}) + random_vec2_offset(1.0)) * ({distortion}))" if distortion != '0.0' else p_expr
if is_color: off_a = 'random_vec2_offset(2.0)'
res = f"vec3({func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec2_offset(2.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec2_offset(3.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}))" off_b = 'random_vec2_offset(3.0)'
else:
res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})"
elif dimensions == '4D': elif dimensions == '4D':
coord_type = 'vec4'
p_expr = f"vec4({co}, {w}) * ({scale})" p_expr = f"vec4({co}, {w}) * ({scale})"
dist_expr = f"({p_expr}) + vec4(snoise(({p_expr}) + random_vec4_offset(0.0)) * ({distortion}), snoise(({p_expr}) + random_vec4_offset(1.0)) * ({distortion}), snoise(({p_expr}) + random_vec4_offset(2.0)) * ({distortion}), snoise(({p_expr}) + random_vec4_offset(3.0)) * ({distortion}))" if distortion != '0.0' else p_expr dist_expr = f"({p_expr}) + vec4(snoise(({p_expr}) + random_vec4_offset(0.0)) * ({distortion}), snoise(({p_expr}) + random_vec4_offset(1.0)) * ({distortion}), snoise(({p_expr}) + random_vec4_offset(2.0)) * ({distortion}), snoise(({p_expr}) + random_vec4_offset(3.0)) * ({distortion}))" if distortion != '0.0' else p_expr
if is_color: off_a = 'random_vec4_offset(4.0)'
res = f"vec3({func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec4_offset(4.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec4_offset(5.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}))" off_b = 'random_vec4_offset(5.0)'
else:
res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})"
else: else:
coord_type = 'vec3'
p_expr = f"({co}) * ({scale})" p_expr = f"({co}) * ({scale})"
dist_expr = f"({p_expr}) + vec3(snoise(({p_expr}) + random_vec3_offset(0.0)) * ({distortion}), snoise(({p_expr}) + random_vec3_offset(1.0)) * ({distortion}), snoise(({p_expr}) + random_vec3_offset(2.0)) * ({distortion}))" if distortion != '0.0' else p_expr dist_expr = f"({p_expr}) + vec3(snoise(({p_expr}) + random_vec3_offset(0.0)) * ({distortion}), snoise(({p_expr}) + random_vec3_offset(1.0)) * ({distortion}), snoise(({p_expr}) + random_vec3_offset(2.0)) * ({distortion}))" if distortion != '0.0' else p_expr
if is_color: off_a = 'random_vec3_offset(3.0)'
res = f"vec3({func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec3_offset(3.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec3_offset(4.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}))" off_b = 'random_vec3_offset(4.0)'
else:
res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})" state.curshader.add_function('// ' + func_name + ' ' + coord_type + '\n' + fractal_src.replace('@T@', coord_type))
co_var = c.node_uid(node) + '_co' + state.get_parser_pass_suffix()
if not c.is_parsed(co_var):
state.parsed.add(co_var)
state.curshader.write(f'{coord_type} {co_var} = {dist_expr};')
fbm_args = f"clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}"
if is_color:
res = f"vec3({func_name}({co_var}, {fbm_args}), {func_name}(({co_var}) + {off_a}, {fbm_args}), {func_name}(({co_var}) + {off_b}, {fbm_args}))"
else:
res = f"{func_name}({co_var}, {fbm_args})"
return res return res
@ -513,7 +526,7 @@ def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket:
state.parsed.add(tex_store) state.parsed.add(tex_store)
tex_name = c.node_name(node.name) tex_name = c.node_uid(node)
tex_link = None tex_link = None
tex_default_file = None tex_default_file = None
is_lnx_mat_param = None is_lnx_mat_param = None
@ -525,8 +538,8 @@ def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket:
image, image,
tex_name, tex_name,
c.mat_get_material(), c.mat_get_material(),
getattr(node, 'interpolation', 'Smart'), node.interpolation,
getattr(node, 'extension', 'REPEAT') node.extension if bpy.app.version >= (3, 1, 0) else 'REPEAT'
) )
if tex is None: if tex is None:
log.warn(f'Object "{state.tree_name}": missing environment texture image "{node.name}"') log.warn(f'Object "{state.tree_name}": missing environment texture image "{node.name}"')
@ -670,6 +683,7 @@ def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.type
outp = 3 outp = 3
m = 0 m = 0
exp = '0.0'
if node.distance == 'MANHATTAN': if node.distance == 'MANHATTAN':
m = 1 m = 1
elif node.distance == 'CHEBYCHEV': elif node.distance == 'CHEBYCHEV':
@ -689,10 +703,13 @@ def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.type
f = 4 f = 4
dim = node.voronoi_dimensions dim = node.voronoi_dimensions
normalize = 1 if node.normalize else 0 normalize = 1 if bpy.app.version >= (4, 0, 0) and node.normalize else 0
c.write_procedurals() c.write_procedurals()
state.curshader.add_function(getattr(c_functions, f'str_tex_voronoi_{bpy.app.version[0]}')) if bpy.app.version >= (5, 0, 0):
state.curshader.add_function(c_functions.str_tex_voronoi_5)
else:
state.curshader.add_function(c_functions.str_tex_voronoi_4)
if node.inputs['Vector'].is_linked: if node.inputs['Vector'].is_linked:
co = c.get_vector_input(node, ['Vector']) co = c.get_vector_input(node, ['Vector'])
@ -718,6 +735,7 @@ def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.type
def parse_tex_wave(node: bpy.types.ShaderNodeTexWave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: def parse_tex_wave(node: bpy.types.ShaderNodeTexWave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
c.write_procedurals() c.write_procedurals()
state.curshader.add_function(c_functions.str_tex_noise) state.curshader.add_function(c_functions.str_tex_noise)
state.curshader.add_function('// noise_fbm vec3\n' + c_functions.str_tex_noise_fbm.replace('@T@', 'vec3'))
state.curshader.add_function(c_functions.str_tex_wave) state.curshader.add_function(c_functions.str_tex_wave)
if node.inputs['Vector'].is_linked: if node.inputs['Vector'].is_linked:
@ -735,9 +753,7 @@ def parse_tex_wave(node: bpy.types.ShaderNodeTexWave, out_socket: bpy.types.Node
dir_map = {'X': 0, 'Y': 1, 'Z': 2, 'DIAGONAL': 3} dir_map = {'X': 0, 'Y': 1, 'Z': 2, 'DIAGONAL': 3}
if hasattr(node, 'wave_direction'): if wave_type == 0:
wave_dir = dir_map.get(node.wave_direction, 0)
elif wave_type == 0:
wave_dir = dir_map.get(node.bands_direction, 0) wave_dir = dir_map.get(node.bands_direction, 0)
else: else:
wave_dir = dir_map.get(node.rings_direction, 0) wave_dir = dir_map.get(node.rings_direction, 0)
@ -804,8 +820,8 @@ def parse_tex_white_noise(node: bpy.types.ShaderNodeTexWhiteNoise, out_socket: b
w = c.parse_value_input(node.inputs['W']) if 'W' in node.inputs else '0.0' w = c.parse_value_input(node.inputs['W']) if 'W' in node.inputs else '0.0'
dimensions = getattr(node, 'noise_dimensions', '3D') dimensions = node.noise_dimensions
is_color = (out_socket == node.outputs[1]) or (getattr(out_socket, 'name', '') == 'Color') is_color = (out_socket == node.outputs[1]) or (out_socket.name == 'Color')
if dimensions == '1D': if dimensions == '1D':
if is_color: if is_color:

View File

@ -28,7 +28,7 @@ def parse_curvevec(node: bpy.types.ShaderNodeVectorCurve, out_socket: bpy.types.
fac = c.get_value_input(node, ['Fac']) fac = c.get_value_input(node, ['Fac'])
vec = c.get_vector_input(node, ['Vector']) vec = c.get_vector_input(node, ['Vector'])
curves = node.mapping.curves curves = node.mapping.curves
name = c.node_name(node.name) name = c.node_uid(node)
res_x = c.vector_curve(name + '0', vec + '.x', curves[0].points) res_x = c.vector_curve(name + '0', vec + '.x', curves[0].points)
res_y = c.vector_curve(name + '1', vec + '.y', curves[1].points) res_y = c.vector_curve(name + '1', vec + '.y', curves[1].points)
@ -47,33 +47,39 @@ def parse_bump(node: bpy.types.ShaderNodeBump, out_socket: bpy.types.NodeSocket,
# Interpolation strength # Interpolation strength
strength = c.get_value_input(node, ['Strength']) strength = c.get_value_input(node, ['Strength'])
distance = c.get_value_input(node, ['Distance']) distance = c.get_value_input(node, ['Distance'])
filter_width = c.get_value_input(node, ['Filter Width']) if 'Filter Width' in node.inputs else '1.0'
height = c.get_value_input(node, ['Height']) height = c.get_value_input(node, ['Height'])
prev_pass = state.current_pass
prev_scale = state.dxdy_scale
state.dxdy_scale = filter_width
state.current_pass = ParserPass.DX_SCREEN_SPACE state.current_pass = ParserPass.DX_SCREEN_SPACE
height_dx = c.get_value_input(node, ['Height']) height_dx = c.get_value_input(node, ['Height'])
state.current_pass = ParserPass.DY_SCREEN_SPACE state.current_pass = ParserPass.DY_SCREEN_SPACE
height_dy = c.get_value_input(node, ['Height']) height_dy = c.get_value_input(node, ['Height'])
state.current_pass = ParserPass.REGULAR state.current_pass = prev_pass
state.dxdy_scale = prev_scale
nor = c.get_vector_input(node, ['Normal']) if node.inputs['Normal'].is_linked else 'n' nor = c.get_vector_input(node, ['Normal']) if node.inputs['Normal'].is_linked else 'n'
if height_dx != height or height_dy != height: if height_dx != height or height_dy != height:
tangent = f'{c.dfdx_fine("wposition")} + {nor} * (({height_dx} - {height}) * {distance})' name = c.node_uid(node)
bitangent = f'{c.dfdy_fine("wposition")} + {nor} * (({height_dy} - {height}) * {distance})' if '__' in name:
name = name.replace('_', '_x')
# Cross-product operand order, dFdy is flipped on d3d11 sfx = state.get_parser_pass_suffix()
bitangent_first = utils.get_gapi() == 'direct3d11' invert = '-1.0' if node.invert else '1.0'
w = state.curshader.write
if node.invert: w(f'vec3 {name}_N{sfx} = normalize({nor});')
bitangent_first = not bitangent_first w(f'float {name}_dist{sfx} = ({distance}) * (gl_FrontFacing ? {invert} : -({invert}));')
w(f'vec3 {name}_dPdx{sfx} = {c.dfdx_fine("wposition")};')
if bitangent_first: w(f'vec3 {name}_dPdy{sfx} = {c.dfdy_fine("wposition")};')
# We need to normalize twice, once for the correct "weight" of the strength, w(f'vec3 {name}_Rx{sfx} = cross({name}_dPdy{sfx}, {name}_N{sfx});')
# once for having a normalized output vector (lerping vectors does not preserve magnitude) w(f'vec3 {name}_Ry{sfx} = cross({name}_N{sfx}, {name}_dPdx{sfx});')
res = f'normalize(mix({nor}, normalize(cross({bitangent}, {tangent})), {strength}))' w(f'float {name}_det{sfx} = dot({name}_dPdx{sfx}, {name}_Rx{sfx});')
else: w(f'vec2 {name}_dHd{sfx} = vec2({height_dx}, {height_dy}) - vec2({height});')
res = f'normalize(mix({nor}, normalize(cross({tangent}, {bitangent})), {strength}))' w(f'vec3 {name}_surfgrad{sfx} = {name}_dHd{sfx}.x * {name}_Rx{sfx} + {name}_dHd{sfx}.y * {name}_Ry{sfx};')
w(f'vec3 {name}_res{sfx} = normalize(({filter_width}) * abs({name}_det{sfx}) * {name}_N{sfx} - {name}_dist{sfx} * sign({name}_det{sfx}) * {name}_surfgrad{sfx});')
res = f'normalize(mix({name}_N{sfx}, {name}_res{sfx}, max({strength}, 0.0)))'
else: else:
res = nor res = nor
@ -96,7 +102,7 @@ def parse_mapping(node: bpy.types.ShaderNodeMapping, out_socket: bpy.types.NodeS
out = f"({out} - {location})" out = f"({out} - {location})"
if input_rotation.is_linked or any(v != 0.0 for v in input_rotation.default_value): if input_rotation.is_linked or any(v != 0.0 for v in input_rotation.default_value):
var_name = c.node_name(node.name) + "_rotation" + state.get_parser_pass_suffix() var_name = c.node_uid(node) + "_rotation" + state.get_parser_pass_suffix()
state.curshader.write(f"mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos({rotation}.x), sin({rotation}.x), 0.0, -sin({rotation}.x), cos({rotation}.x));") state.curshader.write(f"mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos({rotation}.x), sin({rotation}.x), 0.0, -sin({rotation}.x), cos({rotation}.x));")
state.curshader.write(f"mat3 {var_name}Y = mat3(cos({rotation}.y), 0.0, -sin({rotation}.y), 0.0, 1.0, 0.0, sin({rotation}.y), 0.0, cos({rotation}.y));") state.curshader.write(f"mat3 {var_name}Y = mat3(cos({rotation}.y), 0.0, -sin({rotation}.y), 0.0, 1.0, 0.0, sin({rotation}.y), 0.0, cos({rotation}.y));")
state.curshader.write(f"mat3 {var_name}Z = mat3(cos({rotation}.z), sin({rotation}.z), 0.0, -sin({rotation}.z), cos({rotation}.z), 0.0, 0.0, 0.0, 1.0);") state.curshader.write(f"mat3 {var_name}Z = mat3(cos({rotation}.z), sin({rotation}.z), 0.0, -sin({rotation}.z), cos({rotation}.z), 0.0, 0.0, 0.0, 1.0);")
@ -110,7 +116,7 @@ def parse_mapping(node: bpy.types.ShaderNodeMapping, out_socket: bpy.types.NodeS
out = f"({out} * {scale})" out = f"({out} * {scale})"
if input_rotation.is_linked or any(v != 0.0 for v in input_rotation.default_value): if input_rotation.is_linked or any(v != 0.0 for v in input_rotation.default_value):
var_name = c.node_name(node.name) + "_rotation" + state.get_parser_pass_suffix() var_name = c.node_uid(node) + "_rotation" + state.get_parser_pass_suffix()
state.curshader.write(f"mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos({rotation}.x), -sin({rotation}.x), 0.0, sin({rotation}.x), cos({rotation}.x));") state.curshader.write(f"mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos({rotation}.x), -sin({rotation}.x), 0.0, sin({rotation}.x), cos({rotation}.x));")
state.curshader.write(f"mat3 {var_name}Y = mat3(cos({rotation}.y), 0.0, sin({rotation}.y), 0.0, 1.0, 0.0, -sin({rotation}.y), 0.0, cos({rotation}.y));") state.curshader.write(f"mat3 {var_name}Y = mat3(cos({rotation}.y), 0.0, sin({rotation}.y), 0.0, 1.0, 0.0, -sin({rotation}.y), 0.0, cos({rotation}.y));")
state.curshader.write(f"mat3 {var_name}Z = mat3(cos({rotation}.z), -sin({rotation}.z), 0.0, sin({rotation}.z), cos({rotation}.z), 0.0, 0.0, 0.0, 1.0);") state.curshader.write(f"mat3 {var_name}Z = mat3(cos({rotation}.z), -sin({rotation}.z), 0.0, sin({rotation}.z), cos({rotation}.z), 0.0, 0.0, 0.0, 1.0);")
@ -203,7 +209,8 @@ def parse_displacement(node: bpy.types.ShaderNodeDisplacement, out_socket: bpy.t
nor = 'wnormal' nor = 'wnormal'
if node.space == 'OBJECT': if node.space == 'OBJECT':
nor = f'(inverse(mat3(W)) * {nor})' state.curshader.add_uniform('mat4 W', link='_worldMatrix')
nor = f'(transpose(mat3(W)) * {nor})'
disp = f'normalize({nor}) * (vec3({height}) - vec3({midlevel})) * {scale}' disp = f'normalize({nor}) * (vec3({height}) - vec3({midlevel})) * {scale}'
@ -221,12 +228,14 @@ def parse_vector_displacement(node: bpy.types.ShaderNodeVectorDisplacement, out_
offset = f'(({vector} - vec3({midlevel})) * {scale})' offset = f'(({vector} - vec3({midlevel})) * {scale})'
if node.space == 'TANGENT': if node.space == 'TANGENT':
state.curshader.add_uniform('mat4 W', link='_worldMatrix')
t_obj = 'normalize(inverse(mat3(W)) * wtangent)' t_obj = 'normalize(inverse(mat3(W)) * wtangent)'
n_obj = 'normalize(inverse(mat3(W)) * wnormal)' n_obj = 'normalize(transpose(mat3(W)) * wnormal)'
b_obj = f'normalize(cross({n_obj}, {t_obj}))' b_obj = f'normalize(cross({n_obj}, {t_obj}))'
disp = f'({t_obj} * {offset}.x + {n_obj} * {offset}.y + {b_obj} * {offset}.z)' disp = f'({t_obj} * {offset}.x + {b_obj} * {offset}.y + {n_obj} * {offset}.z)'
return f'(vec4(W * vec4({disp}, 0.0)).xyz)' return f'(vec4(W * vec4({disp}, 0.0)).xyz)'
elif node.space == 'OBJECT': elif node.space == 'OBJECT':
state.curshader.add_uniform('mat4 W', link='_worldMatrix')
return f'(vec4(W * vec4({offset}, 0.0)).xyz)' return f'(vec4(W * vec4({offset}, 0.0)).xyz)'
else: else:
return offset return offset
@ -262,3 +271,9 @@ def parse_vectorrotate(node: bpy.types.ShaderNodeVectorRotate, out_socket: bpy.t
return f'vec3( euler_to_mat3({rot_val}) * ({input_vector} - {input_center}) + {input_center})' return f'vec3( euler_to_mat3({rot_val}) * ({input_vector} - {input_center}) + {input_center})'
return f'vec3(0.0, 0.0, 0.0)' return f'vec3(0.0, 0.0, 0.0)'
def parse_bevel(node: bpy.types.ShaderNodeBevel, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
if node.inputs['Normal'].is_linked:
return c.get_vector_input(node, ['Normal'])
return 'n' if state.curshader.shader_type == 'frag' else 'wnormal'

View File

@ -31,7 +31,8 @@ else:
def make(context_id, rpasses, shadowmap=False, shadowmap_transparent=False): def make(context_id, rpasses, shadowmap=False, shadowmap_transparent=False):
is_disp = mat_utils.disp_linked(mat_state.output_node) disp_geom = cycles.disp_mode(mat_state.output_node)[0]
is_disp = disp_geom != 'off'
vs = [{'name': 'pos', 'data': 'short4norm'}] vs = [{'name': 'pos', 'data': 'short4norm'}]
if is_disp or shadowmap_transparent: if is_disp or shadowmap_transparent:
@ -116,7 +117,7 @@ def make(context_id, rpasses, shadowmap=False, shadowmap_transparent=False):
make_particle.write(vert, shadowmap=shadowmap) make_particle.write(vert, shadowmap=shadowmap)
if is_disp: if is_disp:
if rpdat.lnx_rp_displacement == 'Vertex': if disp_geom == 'vertex':
frag.ins = vert.outs frag.ins = vert.outs
vert.add_uniform('mat3 N', '_normalMatrix') vert.add_uniform('mat3 N', '_normalMatrix')
vert.write('vec3 wnormal = normalize(N * vec3(nor.xy, pos.w));') vert.write('vec3 wnormal = normalize(N * vec3(nor.xy, pos.w));')
@ -137,7 +138,7 @@ def make(context_id, rpasses, shadowmap=False, shadowmap_transparent=False):
if con_depth.is_elem('col'): if con_depth.is_elem('col'):
vert.add_out('vec3 vcolor') vert.add_out('vec3 vcolor')
vert.write_attrib('vcolor = col.rgb;') vert.write_attrib('vcolor = col.rgb;')
vert.write('wposition += wnormal * disp;') vert.write('wposition += disp;')
if shadowmap: if shadowmap:
vert.add_uniform('mat4 LVP', '_lightViewProjectionMatrix') vert.add_uniform('mat4 LVP', '_lightViewProjectionMatrix')
vert.write('gl_Position = LVP * vec4(wposition, 1.0);') vert.write('gl_Position = LVP * vec4(wposition, 1.0);')

View File

@ -126,10 +126,11 @@ def make_base(con_mesh, parse_opacity):
vattr_written = False vattr_written = False
rpdat = lnx.utils.get_rp() rpdat = lnx.utils.get_rp()
is_displacement = mat_utils.disp_linked(mat_state.output_node) disp_geom = cycles.disp_mode(mat_state.output_node)[0]
is_displacement = disp_geom != 'off'
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
if is_displacement: if is_displacement:
if rpdat.lnx_rp_displacement == 'Vertex': if disp_geom == 'vertex':
frag.ins = vert.outs frag.ins = vert.outs
else: # Tessellation else: # Tessellation
tesc = con_mesh.make_tesc() tesc = con_mesh.make_tesc()
@ -193,7 +194,7 @@ def make_base(con_mesh, parse_opacity):
vert.write_attrib('TBN = mat3(tangent, bitangent, wnormal);') vert.write_attrib('TBN = mat3(tangent, bitangent, wnormal);')
if is_displacement: if is_displacement:
if rpdat.lnx_rp_displacement == 'Vertex': if disp_geom == 'vertex':
sh = vert sh = vert
else: else:
sh = tese sh = tese

View File

@ -19,8 +19,7 @@ else:
add_mesh_contexts = [] add_mesh_contexts = []
def disp_linked(output_node): def disp_linked(output_node):
linked = output_node.inputs[2].is_linked if not output_node.inputs[2].is_linked:
if not linked:
return False return False
# Leenkx PBR with unlinked height socket # Leenkx PBR with unlinked height socket
l = output_node.inputs[2].links[0] l = output_node.inputs[2].links[0]

View File

@ -143,6 +143,10 @@ ALL_NODES: Dict[str, MaterialNodeMeta] = {
compute_dxdy_variants=ComputeDXDYVariant.NEVER compute_dxdy_variants=ComputeDXDYVariant.NEVER
), ),
'VERTEX_COLOR': MaterialNodeMeta(parse_func=nodes_input.parse_vertex_color), 'VERTEX_COLOR': MaterialNodeMeta(parse_func=nodes_input.parse_vertex_color),
'VOLUME_INFO': MaterialNodeMeta(
parse_func=nodes_input.parse_volumeinfo,
compute_dxdy_variants=ComputeDXDYVariant.NEVER
),
'WIREFRAME': MaterialNodeMeta( 'WIREFRAME': MaterialNodeMeta(
parse_func=nodes_input.parse_wireframe, parse_func=nodes_input.parse_wireframe,
compute_dxdy_variants=ComputeDXDYVariant.NEVER compute_dxdy_variants=ComputeDXDYVariant.NEVER
@ -192,6 +196,7 @@ ALL_NODES: Dict[str, MaterialNodeMeta] = {
'TEX_WHITE_NOISE': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_white_noise), 'TEX_WHITE_NOISE': MaterialNodeMeta(parse_func=nodes_texture.parse_tex_white_noise),
# --- nodes_vector # --- nodes_vector
'BEVEL': MaterialNodeMeta(parse_func=nodes_vector.parse_bevel),
'BUMP': MaterialNodeMeta(parse_func=nodes_vector.parse_bump), 'BUMP': MaterialNodeMeta(parse_func=nodes_vector.parse_bump),
'CURVE_VEC': MaterialNodeMeta(parse_func=nodes_vector.parse_curvevec), 'CURVE_VEC': MaterialNodeMeta(parse_func=nodes_vector.parse_curvevec),
'DISPLACEMENT': MaterialNodeMeta(parse_func=nodes_vector.parse_displacement), 'DISPLACEMENT': MaterialNodeMeta(parse_func=nodes_vector.parse_displacement),
@ -214,7 +219,8 @@ if bpy.app.version > (3, 2, 0):
ALL_NODES['COMBINE_COLOR'] = MaterialNodeMeta(parse_func=nodes_converter.parse_combine_color) ALL_NODES['COMBINE_COLOR'] = MaterialNodeMeta(parse_func=nodes_converter.parse_combine_color)
if bpy.app.version < (4, 0, 0): if bpy.app.version < (4, 0, 0):
ALL_NODES['BSDF_VELVET'] = MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfvelvet) ALL_NODES['BSDF_VELVET'] = MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfvelvet)
if bpy.app.version < (4, 1, 0): ALL_NODES['MIX_RGB'] = MaterialNodeMeta(parse_func=nodes_color.parse_mixrgb)
if bpy.app.version < (4, 2, 0):
ALL_NODES['TEX_MUSGRAVE'] = MaterialNodeMeta(parse_func=nodes_texture.parse_tex_musgrave) ALL_NODES['TEX_MUSGRAVE'] = MaterialNodeMeta(parse_func=nodes_texture.parse_tex_musgrave)
if bpy.app.version >= (4, 0, 0): if bpy.app.version >= (4, 0, 0):
ALL_NODES['BSDF_SHEEN'] = MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfsheen) ALL_NODES['BSDF_SHEEN'] = MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfsheen)

View File

@ -1,5 +1,5 @@
from enum import IntEnum, unique from enum import IntEnum, unique
from typing import List, Set, Tuple, Union, Optional from typing import Dict, List, Set, Tuple, Union, Optional
import bpy import bpy
@ -69,6 +69,9 @@ class ParserState:
# Cache for computing nodes only once # Cache for computing nodes only once
self.parsed: Set[str] = set() self.parsed: Set[str] = set()
self.parsing: Set[str] = set()
self.node_uids: Dict[tuple, str] = {}
self.node_uids_used: Set[str] = set()
# What to parse from the node tree # What to parse from the node tree
self.parse_surface = True self.parse_surface = True
@ -90,6 +93,11 @@ class ParserState:
dx/dy offsets (if required by the parser pass). dx/dy offsets (if required by the parser pass).
""" """
self.dxdy_scale = '1.0'
"""Scale factor applied to screen-space dx/dy offsets, matching
Blender's bump filter_width (offset = dFdx(v) * filter_width).
"""
# Shader output values # Shader output values
self.out_basecol: vec3str = 'vec3(0.8)' self.out_basecol: vec3str = 'vec3(0.8)'
self.out_roughness: floatstr = '0.0' self.out_roughness: floatstr = '0.0'

View File

@ -496,9 +496,10 @@ class LnxRPListItem(bpy.types.PropertyGroup):
name="Diffuse BRDF", description="Diffuse BRDF model", default='Burley', update=update_material_model) name="Diffuse BRDF", description="Diffuse BRDF model", default='Burley', update=update_material_model)
lnx_rp_displacement: EnumProperty( lnx_rp_displacement: EnumProperty(
items=[('Off', 'Off', 'Off'), items=[('Off', 'Off', 'Off'),
('Bump', 'Bump', 'Bump'),
('Vertex', 'Vertex', 'Vertex'), ('Vertex', 'Vertex', 'Vertex'),
('Tessellation', 'Tessellation', 'Tessellation')], ('Tessellation', 'Tessellation', 'Tessellation')],
name="Displacement", description="Enable material displacement", default='Vertex', update=assets.invalidate_shader_cache) name="Displacement", description="Enable material displacement", default='Bump', update=assets.invalidate_shader_cache)
lnx_tess_mesh_inner: IntProperty(name="Inner", description="Inner tessellation level", default=7) lnx_tess_mesh_inner: IntProperty(name="Inner", description="Inner tessellation level", default=7)
lnx_tess_mesh_outer: IntProperty(name="Outer", description="Outer tessellation level", default=7) lnx_tess_mesh_outer: IntProperty(name="Outer", description="Outer tessellation level", default=7)
lnx_tess_shadows_inner: IntProperty(name="Inner", description="Inner tessellation level", default=7) lnx_tess_shadows_inner: IntProperty(name="Inner", description="Inner tessellation level", default=7)