diff --git a/leenkx/blender/lnx/material/cycles.py b/leenkx/blender/lnx/material/cycles.py index 54bc1c3d..84598e44 100644 --- a/leenkx/blender/lnx/material/cycles.py +++ b/leenkx/blender/lnx/material/cycles.py @@ -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) diff --git a/leenkx/blender/lnx/material/cycles_functions.py b/leenkx/blender/lnx/material/cycles_functions.py index e02c0326..581bcc45 100644 --- a/leenkx/blender/lnx/material/cycles_functions.py +++ b/leenkx/blender/lnx/material/cycles_functions.py @@ -1966,107 +1966,110 @@ float snoise(vec4 p) { p = compatible_mod(p, 100000.0) + precision_correction; return 0.8344 * noise_perlin(p); } +""" -#define DEFINE_NOISE_FRACTAL(T) \\\ -float noise_fbm(T co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) { \\\ - T p = co; \\\ - float fscale = 1.0; \\\ - float amp = 1.0; \\\ - float maxamp = 0.0; \\\ - float sum = 0.0; \\\ - for (int i = 0; i <= int(detail); i++) { \\\ - float t = snoise(fscale * p); \\\ - sum += t * amp; \\\ - maxamp += amp; \\\ - amp *= roughness; \\\ - fscale *= lacunarity; \\\ - } \\\ - float rmd = detail - floor(detail); \\\ - if (rmd != 0.0) { \\\ - float t = snoise(fscale * p); \\\ - float sum2 = sum + t * amp; \\\ - return normalize ? mix(0.5 * sum / maxamp + 0.5, 0.5 * sum2 / (maxamp + amp) + 0.5, rmd) : mix(sum, sum2, rmd); \\\ - } 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; \\\ -} +# Fractal noise variants, emitted on demand per coordinate type. +# @T@ is replaced by the coordinate type (float/vec2/vec3/vec4). +str_tex_noise_fbm = """float noise_fbm(@T@ co, float detail, float roughness, float lacunarity, float offset, float gain, bool normalize) { + @T@ p = co; + float fscale = 1.0; + float amp = 1.0; + float maxamp = 0.0; + float sum = 0.0; + for (int i = 0; i <= int(detail); i++) { + float t = snoise(fscale * p); + sum += t * amp; + maxamp += amp; + amp *= roughness; + fscale *= lacunarity; + } + float rmd = detail - floor(detail); + if (rmd != 0.0) { + float t = snoise(fscale * p); + float sum2 = sum + t * amp; + return normalize ? mix(0.5 * sum / maxamp + 0.5, 0.5 * sum2 / (maxamp + amp) + 0.5, rmd) : mix(sum, sum2, rmd); + } else { + return normalize ? 0.5 * sum / maxamp + 0.5 : sum; + } +}""" -DEFINE_NOISE_FRACTAL(float) -DEFINE_NOISE_FRACTAL(vec2) -DEFINE_NOISE_FRACTAL(vec3) -DEFINE_NOISE_FRACTAL(vec4) +str_tex_noise_multi_fractal = """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; +}""" + +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; } 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 str_wavelength_to_rgb = """ vec3 wavelength_to_rgb(const float t) { diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_color.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_color.py index 2696f5d5..4465a51b 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_color.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_color.py @@ -97,59 +97,76 @@ def _parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSo col1 = c.parse_vector_input(node.inputs[6]) col2 = c.parse_vector_input(node.inputs[7]) - # Store factor in variable for linked factor input - 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]) - + fac = _mixrgb_fac(node, state) if node.clamp_factor: fac = f'clamp({fac}, 0.0, 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': - out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) + return 'mix({0}, {1}, {2})'.format(col1, col2, fac) 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': - out_col = 'mix({0}, {0} * {1}, {2})'.format(col1, col2, fac) + return 'mix({0}, {0} * {1}, {2})'.format(col1, col2, fac) 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': - 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': - 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': - 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': - 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': - 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': 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': - dodge = '{0} / max(vec3(1.0) - {1}, vec3(0.000001))'.format(col1, col2) - out_col = 'mix({0}, {1}, {2})'.format(col1, dodge, fac) + return 'clamp({0} / max(vec3(1.0) - {2} * {1}, vec3(0.000001)), vec3(0.0), vec3(1.0))'.format(col1, col2, fac) elif blend == 'BURN': - burn = 'vec3(1.0) - (vec3(1.0) - {0}) / max({1}, vec3(0.000001))'.format(col1, col2) - out_col = 'mix({0}, {1}, {2})'.format(col1, burn, fac) + 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) 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) - out_col = 'mix({0}, {1}, {2})'.format(col1, soft, fac) + return 'mix({0}, {1}, {2})'.format(col1, soft, fac) elif blend == 'LINEAR_LIGHT': 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']: - 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: - log.warn(f'MixRGB node: unsupported blend type {node.blend_type}.') - out_col = col1 + log.warn(f'MixRGB node: unsupported blend type {blend}.') + 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 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]) vec = c.parse_vector_input(node.inputs[1]) curves = node.mapping.curves - name = c.node_name(node.name) + name = c.node_uid(node) # mapping.curves[0].points[0].handle_type 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, diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_converter.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_converter.py index 8aaec0b0..dd8f47ae 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_converter.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_converter.py @@ -89,7 +89,7 @@ def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.Nod elems = node.color_ramp.elements 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): 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) 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: 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]})' 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};') index = '0' for i in range(1, len(elems)): 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};') if interp == 'CONSTANT': state.curshader.write(f'vec4 {ramp_store} = {cols_var}[{index_var}];') else: - facs_var = c.node_name(node.name).upper() + '_FACS' + facs_var = c.node_uid(node).upper() + '_FACS' if state.current_pass == ParserPass.REGULAR: facs_entries = ', '.join(str(e.position) for e in elems) 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: 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 state.curshader.write(f'const vec3 {hsv_var} = rgb_to_hsv({c.parse_vector_input(node.inputs["Color"])}.rgb);') diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py index c44bc03a..6b127c3f 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py @@ -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: if node.lnx_material_param: - nn = 'param_' + c.node_name(node.name) + nn = 'param_' + c.node_uid(node) v = out_socket.default_value 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) @@ -163,7 +163,17 @@ def parse_geometry(node: bpy.types.ShaderNodeNewGeometry, out_socket: bpy.types. # Incoming elif out_socket == node.outputs[4]: 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 elif out_socket == node.outputs[5]: 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: if node.lnx_material_param: - nn = 'param_' + c.node_name(node.name) + nn = 'param_' + c.node_uid(node) value = node.outputs[0].default_value 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) @@ -463,3 +473,9 @@ def parse_wireframe(node: bpy.types.ShaderNodeWireframe, out_socket: bpy.types.N # node.use_pixel_size # size = c.parse_value_input(node.inputs[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' diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py index 9d7a1cac..323173a6 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py @@ -69,12 +69,14 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, c.parse_shader_input(node.inputs[2]) return - prefix = '' if node.inputs['Fac'].is_linked else 'const ' - fac = c.parse_value_input(node.inputs['Fac']) - fac_var = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix() - fac_inv_var = c.node_name(node.name) + '_fac_inv' - 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)) + fac_var = c.node_uid(node) + '_fac' + state.get_parser_pass_suffix() + fac_inv_var = c.node_uid(node) + '_fac_inv' + state.get_parser_pass_suffix() + if not c.is_parsed(fac_var): + state.parsed.add(fac_var) + prefix = '' if node.inputs['Fac'].is_linked else 'const ' + 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 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: if state.parse_surface: c.write_normal(node.inputs['Normal']) - parametrization = getattr(node, 'parametrization', 'COLOR') + parametrization = node.parametrization if parametrization == 'COLOR': color_socket = node.inputs.get('Color') if color_socket is not None: @@ -457,7 +459,7 @@ if bpy.app.version >= (2, 83, 0): rough_socket = node.inputs.get('Roughness') if rough_socket is not None: 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': radial_rough_socket = node.inputs.get('Radial Roughness') if radial_rough_socket is not None: diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py index 5e3e1e8d..52029cc2 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py @@ -136,7 +136,7 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No else: 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_link = None tex_default_file = None @@ -259,7 +259,7 @@ def parse_tex_magic(node: bpy.types.ShaderNodeTexMagic, out_socket: bpy.types.No 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]: 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' distortion = c.parse_value_input(node.inputs['Distortion']) if 'Distortion' in node.inputs else '0.0' - dimensions = getattr(node, 'noise_dimensions', '3D') - noise_type = getattr(node, 'noise_type', 'FBM') - normalize = 'true' if getattr(node, 'normalize', True) else 'false' + dimensions = node.noise_dimensions + noise_type = node.noise_type if bpy.app.version >= (4, 1, 0) else 'FBM' + normalize = 'true' if bpy.app.version < (4, 0, 0) or node.normalize else 'false' - type_map = { - 'FBM': 'noise_fbm', - 'MULTIFRACTAL': 'noise_multi_fractal', - 'RIDGED_MULTIFRACTAL': 'noise_ridged_multi_fractal', - 'HYBRID_MULTIFRACTAL': 'noise_hybrid_multi_fractal', - 'HETERO_TERRAIN': 'noise_hetero_terrain' - } - func_name = type_map.get(noise_type, 'noise_fbm') + if noise_type == 'MULTIFRACTAL': + func_name = 'noise_multi_fractal' + fractal_src = c_functions.str_tex_noise_multi_fractal + elif noise_type == 'RIDGED_MULTIFRACTAL': + func_name = 'noise_ridged_multi_fractal' + fractal_src = c_functions.str_tex_noise_ridged_multi_fractal + elif noise_type == 'HYBRID_MULTIFRACTAL': + 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': + coord_type = 'float' 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 - if is_color: - 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}))" - else: - res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})" - + off_a = 'random_float_offset(1.0)' + off_b = 'random_float_offset(2.0)' elif dimensions == '2D': + coord_type = 'vec2' 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 - if is_color: - 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}))" - else: - res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})" - + off_a = 'random_vec2_offset(2.0)' + off_b = 'random_vec2_offset(3.0)' elif dimensions == '4D': + coord_type = 'vec4' 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 - if is_color: - 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}))" - else: - res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})" - + off_a = 'random_vec4_offset(4.0)' + off_b = 'random_vec4_offset(5.0)' else: + coord_type = 'vec3' 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 - if is_color: - 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}))" - else: - res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})" + off_a = 'random_vec3_offset(3.0)' + off_b = 'random_vec3_offset(4.0)' + + 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 @@ -513,7 +526,7 @@ def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: state.parsed.add(tex_store) - tex_name = c.node_name(node.name) + tex_name = c.node_uid(node) tex_link = None tex_default_file = None is_lnx_mat_param = None @@ -525,8 +538,8 @@ def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: image, tex_name, c.mat_get_material(), - getattr(node, 'interpolation', 'Smart'), - getattr(node, 'extension', 'REPEAT') + node.interpolation, + node.extension if bpy.app.version >= (3, 1, 0) else 'REPEAT' ) if tex is None: 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 m = 0 + exp = '0.0' if node.distance == 'MANHATTAN': m = 1 elif node.distance == 'CHEBYCHEV': @@ -689,10 +703,13 @@ def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.type f = 4 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() - 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: 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]: c.write_procedurals() 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) 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} - if hasattr(node, 'wave_direction'): - wave_dir = dir_map.get(node.wave_direction, 0) - elif wave_type == 0: + if wave_type == 0: wave_dir = dir_map.get(node.bands_direction, 0) else: 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' - dimensions = getattr(node, 'noise_dimensions', '3D') - is_color = (out_socket == node.outputs[1]) or (getattr(out_socket, 'name', '') == 'Color') + dimensions = node.noise_dimensions + is_color = (out_socket == node.outputs[1]) or (out_socket.name == 'Color') if dimensions == '1D': if is_color: diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_vector.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_vector.py index 2be5f4ce..58d73b60 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_vector.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_vector.py @@ -28,7 +28,7 @@ def parse_curvevec(node: bpy.types.ShaderNodeVectorCurve, out_socket: bpy.types. fac = c.get_value_input(node, ['Fac']) vec = c.get_vector_input(node, ['Vector']) 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_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 strength = c.get_value_input(node, ['Strength']) 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']) + prev_pass = state.current_pass + prev_scale = state.dxdy_scale + state.dxdy_scale = filter_width state.current_pass = ParserPass.DX_SCREEN_SPACE height_dx = c.get_value_input(node, ['Height']) state.current_pass = ParserPass.DY_SCREEN_SPACE 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' if height_dx != height or height_dy != height: - tangent = f'{c.dfdx_fine("wposition")} + {nor} * (({height_dx} - {height}) * {distance})' - bitangent = f'{c.dfdy_fine("wposition")} + {nor} * (({height_dy} - {height}) * {distance})' - - # Cross-product operand order, dFdy is flipped on d3d11 - bitangent_first = utils.get_gapi() == 'direct3d11' - - if node.invert: - bitangent_first = not bitangent_first - - if bitangent_first: - # We need to normalize twice, once for the correct "weight" of the strength, - # once for having a normalized output vector (lerping vectors does not preserve magnitude) - res = f'normalize(mix({nor}, normalize(cross({bitangent}, {tangent})), {strength}))' - else: - res = f'normalize(mix({nor}, normalize(cross({tangent}, {bitangent})), {strength}))' - + name = c.node_uid(node) + if '__' in name: + name = name.replace('_', '_x') + sfx = state.get_parser_pass_suffix() + invert = '-1.0' if node.invert else '1.0' + w = state.curshader.write + w(f'vec3 {name}_N{sfx} = normalize({nor});') + w(f'float {name}_dist{sfx} = ({distance}) * (gl_FrontFacing ? {invert} : -({invert}));') + w(f'vec3 {name}_dPdx{sfx} = {c.dfdx_fine("wposition")};') + w(f'vec3 {name}_dPdy{sfx} = {c.dfdy_fine("wposition")};') + w(f'vec3 {name}_Rx{sfx} = cross({name}_dPdy{sfx}, {name}_N{sfx});') + w(f'vec3 {name}_Ry{sfx} = cross({name}_N{sfx}, {name}_dPdx{sfx});') + w(f'float {name}_det{sfx} = dot({name}_dPdx{sfx}, {name}_Rx{sfx});') + w(f'vec2 {name}_dHd{sfx} = vec2({height_dx}, {height_dy}) - vec2({height});') + 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: res = nor @@ -96,7 +102,7 @@ def parse_mapping(node: bpy.types.ShaderNodeMapping, out_socket: bpy.types.NodeS out = f"({out} - {location})" 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}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);") @@ -110,7 +116,7 @@ def parse_mapping(node: bpy.types.ShaderNodeMapping, out_socket: bpy.types.NodeS out = f"({out} * {scale})" 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}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);") @@ -203,7 +209,8 @@ def parse_displacement(node: bpy.types.ShaderNodeDisplacement, out_socket: bpy.t nor = 'wnormal' 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}' @@ -221,12 +228,14 @@ def parse_vector_displacement(node: bpy.types.ShaderNodeVectorDisplacement, out_ offset = f'(({vector} - vec3({midlevel})) * {scale})' if node.space == 'TANGENT': + state.curshader.add_uniform('mat4 W', link='_worldMatrix') 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}))' - 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)' elif node.space == 'OBJECT': + state.curshader.add_uniform('mat4 W', link='_worldMatrix') return f'(vec4(W * vec4({offset}, 0.0)).xyz)' else: 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(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' diff --git a/leenkx/blender/lnx/material/make_depth.py b/leenkx/blender/lnx/material/make_depth.py index 55f9e54c..eafbc2b2 100644 --- a/leenkx/blender/lnx/material/make_depth.py +++ b/leenkx/blender/lnx/material/make_depth.py @@ -31,7 +31,8 @@ else: 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'}] 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) if is_disp: - if rpdat.lnx_rp_displacement == 'Vertex': + if disp_geom == 'vertex': frag.ins = vert.outs vert.add_uniform('mat3 N', '_normalMatrix') 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'): vert.add_out('vec3 vcolor') vert.write_attrib('vcolor = col.rgb;') - vert.write('wposition += wnormal * disp;') + vert.write('wposition += disp;') if shadowmap: vert.add_uniform('mat4 LVP', '_lightViewProjectionMatrix') vert.write('gl_Position = LVP * vec4(wposition, 1.0);') diff --git a/leenkx/blender/lnx/material/make_mesh.py b/leenkx/blender/lnx/material/make_mesh.py index e60cd896..1fd031bf 100644 --- a/leenkx/blender/lnx/material/make_mesh.py +++ b/leenkx/blender/lnx/material/make_mesh.py @@ -126,10 +126,11 @@ def make_base(con_mesh, parse_opacity): vattr_written = False 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'] if is_displacement: - if rpdat.lnx_rp_displacement == 'Vertex': + if disp_geom == 'vertex': frag.ins = vert.outs else: # Tessellation tesc = con_mesh.make_tesc() @@ -193,7 +194,7 @@ def make_base(con_mesh, parse_opacity): vert.write_attrib('TBN = mat3(tangent, bitangent, wnormal);') if is_displacement: - if rpdat.lnx_rp_displacement == 'Vertex': + if disp_geom == 'vertex': sh = vert else: sh = tese diff --git a/leenkx/blender/lnx/material/mat_utils.py b/leenkx/blender/lnx/material/mat_utils.py index ca9c64a9..acba54f5 100644 --- a/leenkx/blender/lnx/material/mat_utils.py +++ b/leenkx/blender/lnx/material/mat_utils.py @@ -19,8 +19,7 @@ else: add_mesh_contexts = [] def disp_linked(output_node): - linked = output_node.inputs[2].is_linked - if not linked: + if not output_node.inputs[2].is_linked: return False # Leenkx PBR with unlinked height socket l = output_node.inputs[2].links[0] diff --git a/leenkx/blender/lnx/material/node_meta.py b/leenkx/blender/lnx/material/node_meta.py index 7a85fe87..d2940288 100644 --- a/leenkx/blender/lnx/material/node_meta.py +++ b/leenkx/blender/lnx/material/node_meta.py @@ -143,6 +143,10 @@ ALL_NODES: Dict[str, MaterialNodeMeta] = { compute_dxdy_variants=ComputeDXDYVariant.NEVER ), '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( parse_func=nodes_input.parse_wireframe, 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), # --- nodes_vector + 'BEVEL': MaterialNodeMeta(parse_func=nodes_vector.parse_bevel), 'BUMP': MaterialNodeMeta(parse_func=nodes_vector.parse_bump), 'CURVE_VEC': MaterialNodeMeta(parse_func=nodes_vector.parse_curvevec), '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) if bpy.app.version < (4, 0, 0): 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) if bpy.app.version >= (4, 0, 0): ALL_NODES['BSDF_SHEEN'] = MaterialNodeMeta(parse_func=nodes_shader.parse_bsdfsheen) diff --git a/leenkx/blender/lnx/material/parser_state.py b/leenkx/blender/lnx/material/parser_state.py index 2a990b93..c0aa07ed 100644 --- a/leenkx/blender/lnx/material/parser_state.py +++ b/leenkx/blender/lnx/material/parser_state.py @@ -1,5 +1,5 @@ from enum import IntEnum, unique -from typing import List, Set, Tuple, Union, Optional +from typing import Dict, List, Set, Tuple, Union, Optional import bpy @@ -69,6 +69,9 @@ class ParserState: # Cache for computing nodes only once 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 self.parse_surface = True @@ -90,6 +93,11 @@ class ParserState: 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 self.out_basecol: vec3str = 'vec3(0.8)' self.out_roughness: floatstr = '0.0' diff --git a/leenkx/blender/lnx/props_renderpath.py b/leenkx/blender/lnx/props_renderpath.py index 45952d09..cdf18a2b 100644 --- a/leenkx/blender/lnx/props_renderpath.py +++ b/leenkx/blender/lnx/props_renderpath.py @@ -496,9 +496,10 @@ class LnxRPListItem(bpy.types.PropertyGroup): name="Diffuse BRDF", description="Diffuse BRDF model", default='Burley', update=update_material_model) lnx_rp_displacement: EnumProperty( items=[('Off', 'Off', 'Off'), + ('Bump', 'Bump', 'Bump'), ('Vertex', 'Vertex', 'Vertex'), ('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_outer: IntProperty(name="Outer", description="Outer tessellation level", default=7) lnx_tess_shadows_inner: IntProperty(name="Inner", description="Inner tessellation level", default=7)