diff --git a/Layer.py b/Layer.py index e5c82328..29475586 100644 --- a/Layer.py +++ b/Layer.py @@ -1,7 +1,7 @@ import bpy, time, re, os, random, numpy from bpy.props import * from bpy_extras.io_utils import ImportHelper -from . import Modifier, lib, Mask, transition, ImageAtlas, UDIM, NormalMapModifier, ListItem, BaseOperator, Decal +from . import Modifier, lib, Mask, transition, ImageAtlas, UDIM, NormalMapModifier, ListItem, BaseOperator, Decal, Triplanar from .common import * #from . import bake_common from .node_arrangements import * @@ -34,6 +34,9 @@ def get_normal_map_type_items(self, context): def check_layer_source(layer, tree=None, image=None, vcol=None, setup_edge_detect=True): if tree == None: tree = get_tree(layer) + # Source can be inside a group + tree = get_source_tree(layer, tree) + # Add source if layer.type == 'VCOL': source, dirty = check_new_node(tree, layer, 'source', get_vcol_bl_idname(), 'Source', True) @@ -4680,7 +4683,7 @@ def replace_layer_type(layer, new_type, item_name='', remove_data=False): setattr(layer, 'cache_' + layer.type.lower(), source.name) # Remove uv input link if any(source.inputs) and any(source.inputs[0].links): - tree.links.remove(source.inputs[0].links[0]) + source_tree.links.remove(source.inputs[0].links[0]) source.label = '' else: remove_node(source_tree, layer, 'source', remove_data=remove_data) @@ -5213,14 +5216,18 @@ def duplicate_layer_nodes_and_images(tree, specific_layers=[], packed_duplicate= if layer.source_group != '': source_group = ttree.nodes.get(layer.source_group) source_group.node_tree = source_group.node_tree.copy() - source = source_group.node_tree.nodes.get(layer.source) + + # Triplanar wrapper keeps the actual source tree inside + inner_tree = Triplanar.duplicate_triplanar_inner_tree(source_group.node_tree) + source_tree = inner_tree if inner_tree else source_group.node_tree + source = source_tree.nodes.get(layer.source) for d in neighbor_directions: s = ttree.nodes.get(getattr(layer, 'source_' + d)) if s: s.node_tree = source_group.node_tree # Duplicate layer modifier groups - duplicate_layer_modifier_tree(layer, source_group.node_tree) + duplicate_layer_modifier_tree(layer, source_tree) else: source = ttree.nodes.get(layer.source) @@ -5294,7 +5301,11 @@ def duplicate_layer_nodes_and_images(tree, specific_layers=[], packed_duplicate= if mask.group_node != '': mask_group = ttree.nodes.get(mask.group_node) mask_group.node_tree = mask_group.node_tree.copy() - mask_source = mask_group.node_tree.nodes.get(mask.source) + + # Triplanar wrapper keeps the actual source tree inside + inner_tree = Triplanar.duplicate_triplanar_inner_tree(mask_group.node_tree) + mask_tree = inner_tree if inner_tree else mask_group.node_tree + mask_source = mask_tree.nodes.get(mask.source) for d in neighbor_directions: s = ttree.nodes.get(getattr(mask, 'source_' + d)) @@ -7417,7 +7428,7 @@ def update_layer_use_baked(self, context): reconnect_yp_nodes(self.id_data) rearrange_yp_nodes(self.id_data) -class YLayer(bpy.types.PropertyGroup, Decal.BaseDecal): +class YLayer(bpy.types.PropertyGroup, Decal.BaseDecal, Triplanar.BaseTriplanar): name : StringProperty( name = 'Layer Name', description = 'Layer name', diff --git a/Mask.py b/Mask.py index b361b2af..2ec12ea0 100644 --- a/Mask.py +++ b/Mask.py @@ -1,7 +1,7 @@ import bpy, re, time, random from bpy.props import * from bpy_extras.io_utils import ImportHelper -from . import lib, ImageAtlas, MaskModifier, UDIM, ListItem, BaseOperator, Decal +from . import lib, ImageAtlas, MaskModifier, UDIM, ListItem, BaseOperator, Decal, Triplanar from .common import * from .node_connections import * from .node_arrangements import * @@ -2444,7 +2444,7 @@ def update_mask_uniform_scale_enabled(self, context): reconnect_layer_nodes(layer) rearrange_layer_nodes(layer) -class YLayerMask(bpy.types.PropertyGroup, Decal.BaseDecal): +class YLayerMask(bpy.types.PropertyGroup, Decal.BaseDecal, Triplanar.BaseTriplanar): name : StringProperty( name = 'Mask Name', diff --git a/Triplanar.py b/Triplanar.py new file mode 100644 index 00000000..dafdc06b --- /dev/null +++ b/Triplanar.py @@ -0,0 +1,253 @@ +import bpy, re +from . import lib +from bpy.props import * +from .common import * +from .node_connections import create_link + +def refresh_triplanar_wrapper(wrapper, source_tree): + ''' Make sure triplanar wrapper interface mirrors the source tree outputs + and its internal nodes sample the source tree three times ''' + + # Vector input should be the first socket + inp = get_tree_input_by_name(wrapper, 'Vector') + if not inp: new_tree_input(wrapper, 'Vector', 'NodeSocketVector') + + valid_input_names = ['Vector'] + list(triplanar_input_props) + for socket_name in triplanar_input_props: + inp = get_tree_input_by_name(wrapper, socket_name) + if not inp: + inp = new_tree_input(wrapper, socket_name, 'NodeSocketFloatFactor') + inp.min_value = 0.0 + inp.max_value = 1.0 + inp.default_value = triplanar_input_defaults.get(socket_name, 1.0) + + for inp in reversed(get_tree_inputs(wrapper)): + if inp.name not in valid_input_names: + remove_tree_input(wrapper, inp) + + # Mirror source tree outputs + valid_output_names = [] + for outp in get_tree_outputs(source_tree): + tout = get_tree_output_by_name(wrapper, outp.name) + if not tout: + socket_type = 'NodeSocketFloat' if outp.name.endswith(io_suffix['ALPHA']) or outp.name == 'Value' else 'NodeSocketColor' + new_tree_output(wrapper, outp.name, socket_type) + valid_output_names.append(outp.name) + + for outp in reversed(get_tree_outputs(wrapper)): + if outp.name not in valid_output_names: + remove_tree_output(wrapper, outp) + + start = wrapper.nodes.get(TREE_START) + end = wrapper.nodes.get(TREE_END) + + # Prep node + prep = wrapper.nodes.get(TRIPLANAR_PREP) + if not prep: + prep = wrapper.nodes.new('ShaderNodeGroup') + prep.name = TRIPLANAR_PREP + prep.label = TRIPLANAR_PREP + prep_tree = lib.get_triplanar_prep_tree() + if prep.node_tree != prep_tree: prep.node_tree = prep_tree + + create_link(wrapper, start.outputs['Vector'], prep.inputs['Vector']) + for socket_name in triplanar_input_props: + create_link(wrapper, start.outputs[socket_name], prep.inputs[socket_name]) + + # Source instances + sources = {} + for i, axis in enumerate(triplanar_axes): + source = wrapper.nodes.get(TRIPLANAR_SOURCE_PREFIX + axis) + if not source: + source = wrapper.nodes.new('ShaderNodeGroup') + source.name = TRIPLANAR_SOURCE_PREFIX + axis + source.label = TRIPLANAR_SOURCE_PREFIX + axis + if source.node_tree != source_tree: source.node_tree = source_tree + source.location = (400, -i * 300) + create_link(wrapper, prep.outputs['Vector ' + axis], source.inputs[0]) + sources[axis] = source + + # Blend node per source tree output pair + alpha_pair_names = [name + io_suffix['ALPHA'] for name in valid_output_names] + main_output_names = [name for name in valid_output_names if name not in alpha_pair_names] + valid_blend_names = [] + for i, name in enumerate(main_output_names): + alpha_name = name + io_suffix['ALPHA'] + paired = alpha_name in valid_output_names + + blend = wrapper.nodes.get(TRIPLANAR_BLEND_PREFIX + name) + if not blend: + blend = wrapper.nodes.new('ShaderNodeGroup') + blend.name = TRIPLANAR_BLEND_PREFIX + name + blend.label = TRIPLANAR_BLEND_PREFIX + name + blend_tree = lib.get_triplanar_blend_tree() if paired else lib.get_triplanar_blend_value_tree() + if blend.node_tree != blend_tree: blend.node_tree = blend_tree + blend.location = (700, -i * 300) + valid_blend_names.append(blend.name) + + for axis in triplanar_axes: + if paired: + create_link(wrapper, sources[axis].outputs[name], blend.inputs['Color ' + axis]) + create_link(wrapper, sources[axis].outputs[alpha_name], blend.inputs['Alpha ' + axis]) + create_link(wrapper, prep.outputs['Color Weight ' + axis], blend.inputs['Color Weight ' + axis]) + create_link(wrapper, prep.outputs['Alpha Weight ' + axis], blend.inputs['Alpha Weight ' + axis]) + else: + create_link(wrapper, sources[axis].outputs[name], blend.inputs['Value ' + axis]) + create_link(wrapper, prep.outputs['Alpha Weight ' + axis], blend.inputs['Weight ' + axis]) + + if paired: + create_link(wrapper, blend.outputs['Color'], end.inputs[name]) + create_link(wrapper, blend.outputs['Alpha'], end.inputs[alpha_name]) + else: + create_link(wrapper, blend.outputs['Value'], end.inputs[name]) + + # Remove unused blend nodes + for node in [n for n in wrapper.nodes if n.name.startswith(TRIPLANAR_BLEND_PREFIX)]: + if node.name not in valid_blend_names: + wrapper.nodes.remove(node) + + start.location = (0, 0) + prep.location = (200, 0) + end.location = (1000, 0) + +def get_triplanar_instances(entity, tree): + ''' Get all group nodes sharing the entity source tree ''' + + m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) + group_prop = 'group_node' if m2 else 'source_group' + + instances = [] + names = [getattr(entity, group_prop)] + [getattr(entity, 'source_' + d) for d in neighbor_directions] + for name in names: + node = tree.nodes.get(name) + if node and node.type == 'GROUP' and node.node_tree: + instances.append(node) + + return instances + +def check_entity_triplanar_nodes(entity, tree=None): + ''' Wrap or unwrap the entity source tree with a triplanar wrapper ''' + + yp = entity.id_data.yp + + m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) + m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) + + if m1: + entity_enabled = get_layer_enabled(entity) + layer = entity + prefix = LAYERGROUP_PREFIX + elif m2: + entity_enabled = get_mask_enabled(entity) + layer = yp.layers[int(m2.group(1))] + prefix = MASKGROUP_PREFIX + else: return + + if not tree: tree = get_tree(layer) + if not tree: return + + instances = get_triplanar_instances(entity, tree) + if not instances: return + + # Instances can point to the source tree or a wrapper around it + current_tree = instances[0].node_tree + source_tree = get_triplanar_source_tree(current_tree) + wrapper = current_tree if source_tree else None + if not source_tree: source_tree = current_tree + + if entity_enabled and is_entity_using_triplanar(entity): + if not wrapper: + wrapper = bpy.data.node_groups.new(prefix + entity.name + ' Triplanar', 'ShaderNodeTree') + create_essential_nodes(wrapper) + + refresh_triplanar_wrapper(wrapper, source_tree) + + for instance in instances: + if instance.node_tree != wrapper: instance.node_tree = wrapper + + elif wrapper: + for instance in instances: + if instance.node_tree != source_tree: instance.node_tree = source_tree + + remove_datablock(bpy.data.node_groups, wrapper) + +def unwrap_triplanar_group_node(group_node): + ''' Point the group node back to the actual source tree and delete the wrapper ''' + + if not group_node or group_node.type != 'GROUP': return + source_tree = get_triplanar_source_tree(group_node.node_tree) + if source_tree: + wrapper = group_node.node_tree + group_node.node_tree = source_tree + remove_datablock(bpy.data.node_groups, wrapper) + +def duplicate_triplanar_inner_tree(wrapper): + ''' Duplicate the source tree inside a copied triplanar wrapper so it's no longer shared ''' + + inner_tree = get_triplanar_source_tree(wrapper) + if not inner_tree: return None + + inner_copy = inner_tree.copy() + for axis in triplanar_axes: + source = wrapper.nodes.get(TRIPLANAR_SOURCE_PREFIX + axis) + if source: source.node_tree = inner_copy + + return inner_copy + +class BaseTriplanar(): + + triplanar_blend : FloatProperty( + name = 'Triplanar Blend', + description = 'Blend smoothness between triplanar projection axes', + subtype = 'FACTOR', + min=0.0, max=1.0, default=0.2, precision=3 + ) + + triplanar_expand : FloatProperty( + name = 'Triplanar Expand', + description = 'Expand the coverage of shown sides to counteract fading when some sides are hidden', + subtype = 'FACTOR', + min=0.0, max=1.0, default=0.0, precision=3 + ) + + triplanar_show_pos_x : FloatProperty( + name = 'Show +X', + description = 'Show triplanar projection on faces pointing along the object +X axis', + subtype = 'FACTOR', + min=0.0, max=1.0, default=1.0, precision=3 + ) + + triplanar_show_neg_x : FloatProperty( + name = 'Show -X', + description = 'Show triplanar projection on faces pointing along the object -X axis', + subtype = 'FACTOR', + min=0.0, max=1.0, default=1.0, precision=3 + ) + + triplanar_show_pos_y : FloatProperty( + name = 'Show +Y', + description = 'Show triplanar projection on faces pointing along the object +Y axis', + subtype = 'FACTOR', + min=0.0, max=1.0, default=1.0, precision=3 + ) + + triplanar_show_neg_y : FloatProperty( + name = 'Show -Y', + description = 'Show triplanar projection on faces pointing along the object -Y axis', + subtype = 'FACTOR', + min=0.0, max=1.0, default=1.0, precision=3 + ) + + triplanar_show_pos_z : FloatProperty( + name = 'Show +Z', + description = 'Show triplanar projection on faces pointing along the object +Z axis', + subtype = 'FACTOR', + min=0.0, max=1.0, default=1.0, precision=3 + ) + + triplanar_show_neg_z : FloatProperty( + name = 'Show -Z', + description = 'Show triplanar projection on faces pointing along the object -Z axis', + subtype = 'FACTOR', + min=0.0, max=1.0, default=1.0, precision=3 + ) diff --git a/__init__.py b/__init__.py index 6503b266..f0004f00 100644 --- a/__init__.py +++ b/__init__.py @@ -28,6 +28,7 @@ def is_available(module_relpath): importlib.reload(bake_common) importlib.reload(modifier_common) importlib.reload(Decal) + importlib.reload(Triplanar) importlib.reload(ui) importlib.reload(subtree) importlib.reload(transition_common) @@ -63,7 +64,7 @@ def is_available(module_relpath): from . import lib from . import BaseOperator from . import Localization - from . import image_ops, bake_common, modifier_common, Decal, ui, subtree, transition_common, input_outputs, node_arrangements, node_connections + from . import image_ops, bake_common, modifier_common, Decal, Triplanar, ui, subtree, transition_common, input_outputs, node_arrangements, node_connections from . import vector_displacement_lib, vector_displacement from . import vcol_editor, transition, BakeTarget, BakeInfo, UDIM, ImageAtlas, MaskModifier, Mask, Modifier, NormalMapModifier, Layer, ListItem, Bake, BakeToLayer, Root, versioning if is_available('.addon_updater_ops'): from . import addon_updater_ops diff --git a/common.py b/common.py index b4217f3b..044706e5 100644 --- a/common.py +++ b/common.py @@ -51,6 +51,33 @@ TEMP_ACTIVE_IMAGE_NAME = '.YP_TEMP_ACTIVE_IMAGE' TEMP_ACTIVE_IMAGE_NODE_NAME = '.YP_TEMP_ACTIVE_IMAGE_NODE' +TRIPLANAR_PREP = 'Triplanar Prep' +TRIPLANAR_BLEND_PREFIX = 'Triplanar Blend ' +TRIPLANAR_SOURCE_PREFIX = 'Triplanar Source ' +triplanar_axes = ['X', 'Y', 'Z'] +triplanar_side_props = { + '+X' : 'triplanar_show_pos_x', + '-X' : 'triplanar_show_neg_x', + '+Y' : 'triplanar_show_pos_y', + '-Y' : 'triplanar_show_neg_y', + '+Z' : 'triplanar_show_pos_z', + '-Z' : 'triplanar_show_neg_z', +} + +# Triplanar wrapper input socket names paired with their entity properties +triplanar_input_props = { + 'Blend' : 'triplanar_blend', + 'Expand' : 'triplanar_expand', +} +for side, prop in triplanar_side_props.items(): + triplanar_input_props['Show ' + side] = prop + +# Show side inputs default to 1.0 +triplanar_input_defaults = { + 'Blend' : 0.2, + 'Expand' : 0.0, +} + def is_bl_newer_than(major, minor=0, patch=0): return bpy.app.version >= (major, minor, patch) @@ -507,6 +534,7 @@ def get_vertex_color_label(capital=11): ('Window', 'Window', ''), ('Reflection', 'Reflection', ''), ('Decal', 'Decal', ''), + ('Triplanar', 'Triplanar', ''), ) mask_texcoord_type_items = ( @@ -518,6 +546,7 @@ def get_vertex_color_label(capital=11): ('Window', 'Window', ''), ('Reflection', 'Reflection', ''), ('Decal', 'Decal', ''), + ('Triplanar', 'Triplanar', ''), ('Layer', 'Use Layer Vector', ''), ) @@ -617,6 +646,7 @@ def get_vertex_color_label(capital=11): 'Window' : 'Texcoord Window', 'Reflection' : 'Texcoord Reflection', 'Decal' : 'Texcoord Object', + 'Triplanar' : 'Texcoord Object', } math_method_items = ( @@ -2360,6 +2390,14 @@ def get_mod_tree(entity): return tree +def get_triplanar_source_tree(tree): + ''' Get the actual source tree if the passed tree is a triplanar wrapper ''' + if not tree: return None + source_x = tree.nodes.get(TRIPLANAR_SOURCE_PREFIX + 'X') + if source_x and source_x.type == 'GROUP' and source_x.node_tree: + return source_x.node_tree + return None + def get_mask_tree(mask, layer_tree=None, ignore_group=False): if not layer_tree: @@ -2379,6 +2417,8 @@ def get_mask_tree(mask, layer_tree=None, ignore_group=False): else: return None if not group_node or group_node.type != 'GROUP': return layer_tree + inner_tree = get_triplanar_source_tree(group_node.node_tree) + if inner_tree: return inner_tree return group_node.node_tree def get_mask_source(mask, get_baked=False, layer_tree=None): @@ -2428,6 +2468,8 @@ def get_channel_source_tree(ch, layer=None, tree=None): if ch.source_group != '': source_group = tree.nodes.get(ch.source_group) if source_group: + inner_tree = get_triplanar_source_tree(source_group.node_tree) + if inner_tree: return inner_tree return source_group.node_tree return tree @@ -2468,6 +2510,8 @@ def get_source_tree(layer, tree=None): if layer.source_group != '': source_group = tree.nodes.get(layer.source_group) + inner_tree = get_triplanar_source_tree(source_group.node_tree) + if inner_tree: return inner_tree return source_group.node_tree return tree @@ -3441,7 +3485,10 @@ def get_udim_segment_mapping_offset(segment): offset_y += tiles_height + 1 def is_mapping_possible(entity_type): - return entity_type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER', 'AO'} + return entity_type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER', 'AO'} + +def is_entity_using_triplanar(entity): + return entity.texcoord_type == 'Triplanar' and not entity.use_baked and is_mapping_possible(entity.type) def clear_mapping(entity, use_baked=False): diff --git a/input_outputs.py b/input_outputs.py index 5e2547df..9ab49527 100644 --- a/input_outputs.py +++ b/input_outputs.py @@ -1,5 +1,5 @@ import bpy, re -from . import lib, Decal +from . import lib, Decal, Triplanar from .common import * from .transition_common import * from .subtree import * @@ -575,6 +575,13 @@ def check_all_layer_channel_io_and_nodes(layer, tree=None, specific_ch=None, do_ Decal.check_entity_decal_nodes(mask, tree) #check_mask_image_linear_node(mask) + # Triplanar wrappers need source trees, which are only checked on normal channel updates + check_layer_source_tree(layer) + check_mask_source_tree(layer) + Triplanar.check_entity_triplanar_nodes(layer, tree) + for mask in layer.masks: + Triplanar.check_entity_triplanar_nodes(mask, tree) + # Linear nodes check_yp_linear_nodes(yp, layer, False) @@ -744,7 +751,12 @@ def check_layer_tree_ios(layer, tree=None, remove_props=False, hard_reset=False) if layer.texcoord_type == 'Decal': dirty = create_prop_input(layer, 'decal_distance_value', valid_inputs, input_index, dirty, float_factor_input_names) input_index += 1 - + + elif layer.texcoord_type == 'Triplanar': + for prop_name in triplanar_input_props.values(): + dirty = create_prop_input(layer, prop_name, valid_inputs, input_index, dirty, float_factor_input_names) + input_index += 1 + if is_bl_newer_than(2, 81) and layer.enable_uniform_scale and is_layer_using_vector(layer) and layer.segment_name == '': dirty = create_prop_input(layer, 'uniform_scale_value', valid_inputs, input_index, dirty, float_factor_input_names) input_index += 1 @@ -894,6 +906,12 @@ def check_layer_tree_ios(layer, tree=None, remove_props=False, hard_reset=False) dirty = create_prop_input(mask, 'decal_distance_value', valid_inputs, input_index, dirty, float_factor_input_names) input_index += 1 + # Mask triplanar + elif mask.texcoord_type == 'Triplanar': + for prop_name in triplanar_input_props.values(): + dirty = create_prop_input(mask, prop_name, valid_inputs, input_index, dirty, float_factor_input_names) + input_index += 1 + # Color ID if mask.type == 'COLOR_ID': dirty = create_prop_input(mask, 'color_id', valid_inputs, input_index, dirty, float_factor_input_names) diff --git a/lib.py b/lib.py index b7df289e..7691a77c 100644 --- a/lib.py +++ b/lib.py @@ -151,6 +151,11 @@ DECAL_PROCESS = '~yPL Decal Process' +TRIPLANAR = '~yPL Triplanar' +TRIPLANAR_BLEND = '~yPL Triplanar Blend' +TRIPLANAR_BLEND_VALUE = '~yPL Triplanar Blend Value' +TRIPLANAR_REVISION_MARKER = '__yp_triplanar_rev_3' + SMOOTH_PREFIX = '~yPL Smooth ' # Nodes that require Blender 2.81 at minimum @@ -379,7 +384,7 @@ def get_neighbor_uv_tree_name(texcoord_type, entity): different_uv = check_uv_difference_to_main_uv(entity) if different_uv: return NEIGHBOR_UV_OTHER_UV return NEIGHBOR_UV_TANGENT - if texcoord_type in {'Generated', 'Normal', 'Object', 'Decal'}: + if texcoord_type in {'Generated', 'Normal', 'Object', 'Decal', 'Triplanar'}: return NEIGHBOR_UV_OBJECT if texcoord_type in {'Camera', 'Window', 'Reflection'}: return NEIGHBOR_UV_CAMERA @@ -506,6 +511,325 @@ def get_smooth_mix_node(blend_type, layer_type=''): return tree +def new_math_node(tree, operation, loc, value=None): + node = tree.nodes.new('ShaderNodeMath') + node.operation = operation + node.location = loc.copy() + if value != None: node.inputs[1].default_value = value + return node + +def get_triplanar_prep_tree(): + ''' Convert a vector to three planar projection vectors and their blend weights. + Weights use object space normal components normalized by their maximum, + so they stay stable at any blend sharpness. Color weights are normalized by the + sum of shown sides, while alpha weights are normalized by the sum of all sides, + so hiding a side fades the entity out on faces pointing that way. ''' + + tree = bpy.data.node_groups.get(TRIPLANAR) + if tree: + if tree.nodes.get(TRIPLANAR_REVISION_MARKER): return tree + + # Rebuild the tree since it comes from an older revision + tree.nodes.clear() + for inp in reversed(get_tree_inputs(tree)): remove_tree_input(tree, inp) + for outp in reversed(get_tree_outputs(tree)): remove_tree_output(tree, outp) + else: + tree = bpy.data.node_groups.new(TRIPLANAR, 'ShaderNodeTree') + + # IO + new_tree_input(tree, 'Vector', 'NodeSocketVector') + for socket_name in triplanar_input_props: + inp = new_tree_input(tree, socket_name, 'NodeSocketFloatFactor') + inp.min_value = 0.0 + inp.max_value = 1.0 + inp.default_value = triplanar_input_defaults.get(socket_name, 1.0) + + for axis in triplanar_axes: + new_tree_output(tree, 'Vector ' + axis, 'NodeSocketVector') + for axis in triplanar_axes: + new_tree_output(tree, 'Color Weight ' + axis, 'NodeSocketFloat') + for axis in triplanar_axes: + new_tree_output(tree, 'Alpha Weight ' + axis, 'NodeSocketFloat') + + create_essential_nodes(tree) + + start = tree.nodes.get(TREE_START) + end = tree.nodes.get(TREE_END) + + loc = Vector((0, 0)) + start.location = loc + + # Projection vectors + loc.x += 200 + sep_pos = tree.nodes.new('ShaderNodeSeparateXYZ') + sep_pos.location = loc + tree.links.new(start.outputs['Vector'], sep_pos.inputs[0]) + + loc.x += 200 + swizzles = {'X': ('Y', 'Z'), 'Y': ('X', 'Z'), 'Z': ('X', 'Y')} + for axis in triplanar_axes: + com = tree.nodes.new('ShaderNodeCombineXYZ') + com.location = loc + tree.links.new(sep_pos.outputs[swizzles[axis][0]], com.inputs[0]) + tree.links.new(sep_pos.outputs[swizzles[axis][1]], com.inputs[1]) + tree.links.new(com.outputs[0], end.inputs['Vector ' + axis]) + loc.y -= 150 + + # Object space normal + loc = Vector((0, -600)) + texcoord = tree.nodes.new('ShaderNodeTexCoord') + texcoord.location = loc + + loc.x += 200 + sep_nor = tree.nodes.new('ShaderNodeSeparateXYZ') + sep_nor.location = loc + tree.links.new(texcoord.outputs['Normal'], sep_nor.inputs[0]) + + # Blend factor to weight exponent + exp_max = new_math_node(tree, 'MAXIMUM', loc + Vector((0, -200)), 0.001) + tree.links.new(start.outputs['Blend'], exp_max.inputs[0]) + exponent = new_math_node(tree, 'POWER', loc + Vector((200, -200)), -2.0) + tree.links.new(exp_max.outputs[0], exponent.inputs[0]) + + loc.x += 200 + abses = {} + for i, axis in enumerate(triplanar_axes): + abs_node = new_math_node(tree, 'ABSOLUTE', loc + Vector((0, -i * 150))) + tree.links.new(sep_nor.outputs[axis], abs_node.inputs[0]) + abses[axis] = abs_node + + # Normalize absolute normal by its largest component + loc.x += 200 + max_xy = new_math_node(tree, 'MAXIMUM', loc) + tree.links.new(abses['X'].outputs[0], max_xy.inputs[0]) + tree.links.new(abses['Y'].outputs[0], max_xy.inputs[1]) + max_xyz = new_math_node(tree, 'MAXIMUM', loc + Vector((0, -150))) + tree.links.new(max_xy.outputs[0], max_xyz.inputs[0]) + tree.links.new(abses['Z'].outputs[0], max_xyz.inputs[1]) + max_safe = new_math_node(tree, 'MAXIMUM', loc + Vector((0, -300)), 0.00001) + tree.links.new(max_xyz.outputs[0], max_safe.inputs[0]) + + loc.x += 200 + weights = {} + for i, axis in enumerate(triplanar_axes): + div = new_math_node(tree, 'DIVIDE', loc + Vector((0, -i * 300))) + tree.links.new(abses[axis].outputs[0], div.inputs[0]) + tree.links.new(max_safe.outputs[0], div.inputs[1]) + + power = new_math_node(tree, 'POWER', loc + Vector((200, -i * 300))) + tree.links.new(div.outputs[0], power.inputs[0]) + tree.links.new(exponent.outputs[0], power.inputs[1]) + weights[axis] = power + + # Select show factor by normal direction so each side of the axis can be hidden separately + loc.x += 400 + shown_weights = {} + for i, axis in enumerate(triplanar_axes): + is_positive = new_math_node(tree, 'GREATER_THAN', loc + Vector((0, -i * 300)), 0.0) + tree.links.new(sep_nor.outputs[axis], is_positive.inputs[0]) + + side_diff = new_math_node(tree, 'SUBTRACT', loc + Vector((0, -i * 300 - 150))) + tree.links.new(start.outputs['Show +' + axis], side_diff.inputs[0]) + tree.links.new(start.outputs['Show -' + axis], side_diff.inputs[1]) + + side_select = new_math_node(tree, 'MULTIPLY', loc + Vector((200, -i * 300))) + tree.links.new(is_positive.outputs[0], side_select.inputs[0]) + tree.links.new(side_diff.outputs[0], side_select.inputs[1]) + + show = new_math_node(tree, 'ADD', loc + Vector((200, -i * 300 - 150))) + tree.links.new(start.outputs['Show -' + axis], show.inputs[0]) + tree.links.new(side_select.outputs[0], show.inputs[1]) + + mul = new_math_node(tree, 'MULTIPLY', loc + Vector((400, -i * 300))) + tree.links.new(weights[axis].outputs[0], mul.inputs[0]) + tree.links.new(show.outputs[0], mul.inputs[1]) + shown_weights[axis] = mul + + loc.x += 400 + + # Sum of all weights and sum of shown weights + loc.x += 200 + full_sum = new_math_node(tree, 'ADD', loc) + tree.links.new(weights['X'].outputs[0], full_sum.inputs[0]) + tree.links.new(weights['Y'].outputs[0], full_sum.inputs[1]) + full_sum_1 = new_math_node(tree, 'ADD', loc + Vector((0, -150))) + tree.links.new(full_sum.outputs[0], full_sum_1.inputs[0]) + tree.links.new(weights['Z'].outputs[0], full_sum_1.inputs[1]) + full_sum_safe = new_math_node(tree, 'MAXIMUM', loc + Vector((0, -300)), 0.00001) + tree.links.new(full_sum_1.outputs[0], full_sum_safe.inputs[0]) + + shown_sum = new_math_node(tree, 'ADD', loc + Vector((0, -450))) + tree.links.new(shown_weights['X'].outputs[0], shown_sum.inputs[0]) + tree.links.new(shown_weights['Y'].outputs[0], shown_sum.inputs[1]) + shown_sum_1 = new_math_node(tree, 'ADD', loc + Vector((0, -600))) + tree.links.new(shown_sum.outputs[0], shown_sum_1.inputs[0]) + tree.links.new(shown_weights['Z'].outputs[0], shown_sum_1.inputs[1]) + + # Coverage of shown sides, expandable to counteract fading when some sides are hidden + loc.x += 200 + coverage = new_math_node(tree, 'DIVIDE', loc + Vector((0, -900))) + tree.links.new(shown_sum_1.outputs[0], coverage.inputs[0]) + tree.links.new(full_sum_safe.outputs[0], coverage.inputs[1]) + + expand_inv = new_math_node(tree, 'SUBTRACT', loc + Vector((0, -1050))) + expand_inv.inputs[0].default_value = 1.0 + tree.links.new(start.outputs['Expand'], expand_inv.inputs[1]) + expand_inv_safe = new_math_node(tree, 'MAXIMUM', loc + Vector((200, -1050)), 0.001) + tree.links.new(expand_inv.outputs[0], expand_inv_safe.inputs[0]) + + expanded_coverage = new_math_node(tree, 'DIVIDE', loc + Vector((400, -900))) + tree.links.new(coverage.outputs[0], expanded_coverage.inputs[0]) + tree.links.new(expand_inv_safe.outputs[0], expanded_coverage.inputs[1]) + expanded_coverage_clamped = new_math_node(tree, 'MINIMUM', loc + Vector((600, -900)), 1.0) + tree.links.new(expanded_coverage.outputs[0], expanded_coverage_clamped.inputs[0]) + + # Color always blends all projections to avoid stretching, + # hiding sides only fades the entity out through the alpha weights + for i, axis in enumerate(triplanar_axes): + color_weight = new_math_node(tree, 'DIVIDE', loc + Vector((0, -i * 300))) + tree.links.new(weights[axis].outputs[0], color_weight.inputs[0]) + tree.links.new(full_sum_safe.outputs[0], color_weight.inputs[1]) + tree.links.new(color_weight.outputs[0], end.inputs['Color Weight ' + axis]) + + alpha_weight = new_math_node(tree, 'MULTIPLY', loc + Vector((800, -i * 300))) + tree.links.new(color_weight.outputs[0], alpha_weight.inputs[0]) + tree.links.new(expanded_coverage_clamped.outputs[0], alpha_weight.inputs[1]) + tree.links.new(alpha_weight.outputs[0], end.inputs['Alpha Weight ' + axis]) + + loc.x += 1000 + end.location = loc + + marker = tree.nodes.new('NodeFrame') + marker.name = TRIPLANAR_REVISION_MARKER + marker.label = '' + + return tree + +def get_triplanar_blend_tree(): + ''' Blend three triplanar samples of a color and alpha pair ''' + + tree = bpy.data.node_groups.get(TRIPLANAR_BLEND) + if tree: return tree + + tree = bpy.data.node_groups.new(TRIPLANAR_BLEND, 'ShaderNodeTree') + + # IO + for axis in triplanar_axes: + new_tree_input(tree, 'Color ' + axis, 'NodeSocketColor') + for axis in triplanar_axes: + new_tree_input(tree, 'Alpha ' + axis, 'NodeSocketFloat') + for axis in triplanar_axes: + new_tree_input(tree, 'Color Weight ' + axis, 'NodeSocketFloat') + for axis in triplanar_axes: + new_tree_input(tree, 'Alpha Weight ' + axis, 'NodeSocketFloat') + + new_tree_output(tree, 'Color', 'NodeSocketColor') + new_tree_output(tree, 'Alpha', 'NodeSocketFloat') + + create_essential_nodes(tree) + + start = tree.nodes.get(TREE_START) + end = tree.nodes.get(TREE_END) + + loc = Vector((0, 0)) + start.location = loc + + loc.x += 200 + color_muls = {} + for i, axis in enumerate(triplanar_axes): + mul = simple_new_mix_node(tree) + mixcol0, mixcol1, mixout = get_mix_color_indices(mul) + mul.blend_type = 'MULTIPLY' + mul.inputs[0].default_value = 1.0 + mul.location = loc + Vector((0, -i * 200)) + tree.links.new(start.outputs['Color ' + axis], mul.inputs[mixcol0]) + tree.links.new(start.outputs['Color Weight ' + axis], mul.inputs[mixcol1]) + color_muls[axis] = (mul, mixout) + + loc.x += 200 + add_xy = simple_new_mix_node(tree) + mixcol0, mixcol1, add_xy_out = get_mix_color_indices(add_xy) + add_xy.blend_type = 'ADD' + add_xy.inputs[0].default_value = 1.0 + add_xy.location = loc + tree.links.new(color_muls['X'][0].outputs[color_muls['X'][1]], add_xy.inputs[mixcol0]) + tree.links.new(color_muls['Y'][0].outputs[color_muls['Y'][1]], add_xy.inputs[mixcol1]) + + loc.x += 200 + add_xyz = simple_new_mix_node(tree) + mixcol0, mixcol1, mixout = get_mix_color_indices(add_xyz) + add_xyz.blend_type = 'ADD' + add_xyz.inputs[0].default_value = 1.0 + add_xyz.location = loc + tree.links.new(add_xy.outputs[add_xy_out], add_xyz.inputs[mixcol0]) + tree.links.new(color_muls['Z'][0].outputs[color_muls['Z'][1]], add_xyz.inputs[mixcol1]) + tree.links.new(add_xyz.outputs[mixout], end.inputs['Color']) + + # Alpha + loc = Vector((200, -700)) + alpha_muls = {} + for i, axis in enumerate(triplanar_axes): + mul = new_math_node(tree, 'MULTIPLY', loc + Vector((0, -i * 150))) + tree.links.new(start.outputs['Alpha ' + axis], mul.inputs[0]) + tree.links.new(start.outputs['Alpha Weight ' + axis], mul.inputs[1]) + alpha_muls[axis] = mul + + alpha_add_xy = new_math_node(tree, 'ADD', loc + Vector((200, 0))) + tree.links.new(alpha_muls['X'].outputs[0], alpha_add_xy.inputs[0]) + tree.links.new(alpha_muls['Y'].outputs[0], alpha_add_xy.inputs[1]) + alpha_add_xyz = new_math_node(tree, 'ADD', loc + Vector((400, 0))) + tree.links.new(alpha_add_xy.outputs[0], alpha_add_xyz.inputs[0]) + tree.links.new(alpha_muls['Z'].outputs[0], alpha_add_xyz.inputs[1]) + tree.links.new(alpha_add_xyz.outputs[0], end.inputs['Alpha']) + + end.location = Vector((800, 0)) + + return tree + +def get_triplanar_blend_value_tree(): + ''' Blend three triplanar samples of a single value ''' + + tree = bpy.data.node_groups.get(TRIPLANAR_BLEND_VALUE) + if tree: return tree + + tree = bpy.data.node_groups.new(TRIPLANAR_BLEND_VALUE, 'ShaderNodeTree') + + # IO + for axis in triplanar_axes: + new_tree_input(tree, 'Value ' + axis, 'NodeSocketFloat') + for axis in triplanar_axes: + new_tree_input(tree, 'Weight ' + axis, 'NodeSocketFloat') + + new_tree_output(tree, 'Value', 'NodeSocketFloat') + + create_essential_nodes(tree) + + start = tree.nodes.get(TREE_START) + end = tree.nodes.get(TREE_END) + + loc = Vector((0, 0)) + start.location = loc + + loc.x += 200 + muls = {} + for i, axis in enumerate(triplanar_axes): + mul = new_math_node(tree, 'MULTIPLY', loc + Vector((0, -i * 150))) + tree.links.new(start.outputs['Value ' + axis], mul.inputs[0]) + tree.links.new(start.outputs['Weight ' + axis], mul.inputs[1]) + muls[axis] = mul + + add_xy = new_math_node(tree, 'ADD', loc + Vector((200, 0))) + tree.links.new(muls['X'].outputs[0], add_xy.inputs[0]) + tree.links.new(muls['Y'].outputs[0], add_xy.inputs[1]) + add_xyz = new_math_node(tree, 'ADD', loc + Vector((400, 0))) + tree.links.new(add_xy.outputs[0], add_xyz.inputs[0]) + tree.links.new(muls['Z'].outputs[0], add_xyz.inputs[1]) + tree.links.new(add_xyz.outputs[0], end.inputs['Value']) + + end.location = loc + Vector((600, 0)) + + return tree + def clean_unused_libraries(): for ng in bpy.data.node_groups: if ng.name.startswith('~yPL ') and ng.users == 0: diff --git a/node_connections.py b/node_connections.py index 3060040f..8cfb8662 100644 --- a/node_connections.py +++ b/node_connections.py @@ -1274,11 +1274,13 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): for tc in texcoords: inp = node.inputs.get(io_names[tc]) - if inp: + # Triplanar uses object texture coordinate + outp_name = 'Object' if tc == 'Triplanar' else tc + if inp: if parallax_ch and parallax: - create_link(tree, parallax.outputs[TEXCOORD_IO_PREFIX + tc], inp) - else: - create_link(tree, get_essential_node(tree, TEXCOORD)[tc], inp) + create_link(tree, parallax.outputs[TEXCOORD_IO_PREFIX + outp_name], inp) + else: + create_link(tree, get_essential_node(tree, TEXCOORD)[outp_name], inp) # Background layer if layer.type == 'BACKGROUND': @@ -1712,7 +1714,8 @@ def reconnect_mask_source_nodes(mask, layer_tree): # Mask inside a node group has start and end node group_node = layer_tree.nodes.get(mask.group_node) if group_node: - tree = group_node.node_tree + # Mask tree can be inside a triplanar wrapper + tree = get_mask_tree(mask, layer_tree) start = tree.nodes.get(TREE_START) end = tree.nodes.get(TREE_END) else: @@ -1772,6 +1775,16 @@ def reconnect_mask_source_nodes(mask, layer_tree): return source, val +def reconnect_triplanar_input_links(tree, entity, targets): + ''' Connect triplanar prop inputs to all triplanar wrapper instances ''' + + for socket_name, prop_name in triplanar_input_props.items(): + inp = get_essential_node(tree, TREE_START).get(get_entity_input_name(entity, prop_name)) + if not inp: continue + for target in targets: + if target and socket_name in target.inputs: + create_link(tree, inp, target.inputs[socket_name]) + def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): yp = layer.id_data.yp @@ -1907,6 +1920,9 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): if 'Vector' in source.inputs: create_link(tree, vector, source.inputs['Vector']) + if layer.texcoord_type == 'Triplanar': + reconnect_triplanar_input_links(tree, layer, [source, source_n, source_s, source_e, source_w]) + if layer.use_baked or layer.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'EDGE_DETECT', 'AO'}: if uv_neighbor: @@ -2191,6 +2207,10 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): create_link(tree, mask_vector, mask_source.inputs[0]) + if mask.texcoord_type == 'Triplanar': + triplanar_targets = [mask_source] + [nodes.get(getattr(mask, 'source_' + d)) for d in nsew_letters] + reconnect_triplanar_input_links(tree, mask, triplanar_targets) + # Mask UV uniform scale value if is_bl_newer_than(2, 81): uniform_scale_value = get_essential_node(tree, TREE_START).get(get_entity_input_name(mask, 'uniform_scale_value')) diff --git a/subtree.py b/subtree.py index 16fa6037..88a7e49d 100644 --- a/subtree.py +++ b/subtree.py @@ -1,5 +1,5 @@ import bpy, re, bmesh -from . import lib, Modifier, MaskModifier +from . import lib, Modifier, MaskModifier, Triplanar from .common import * from .node_arrangements import * from .node_connections import * @@ -146,6 +146,8 @@ def disable_layer_source_tree(layer, layer_tree=None, source_group=None): if not layer_tree: layer_tree = get_tree(layer) if not source_group: source_group = layer_tree.nodes.get(layer.source_group) + Triplanar.unwrap_triplanar_group_node(source_group) + if source_group: source_ref = source_group.node_tree.nodes.get(layer.source) baked_source_ref = source_group.node_tree.nodes.get(layer.baked_source) @@ -187,12 +189,16 @@ def disable_layer_source_tree(layer, layer_tree=None, source_group=None): remove_node(layer_tree, layer, 'source_e') remove_node(layer_tree, layer, 'source_w') -def check_layer_source_tree(layer, smooth_bump_enabled): +def check_layer_source_tree(layer, smooth_bump_enabled=None): + + if smooth_bump_enabled == None: + smooth_bump_ch = get_smooth_bump_channel(layer) + smooth_bump_enabled = smooth_bump_ch != None and get_channel_enabled(smooth_bump_ch) and is_height_process_needed(layer) layer_tree = get_tree(layer) source_group = layer_tree.nodes.get(layer.source_group) - if (smooth_bump_enabled and + if ((smooth_bump_enabled or is_entity_using_triplanar(layer)) and (layer.use_baked or layer.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER'}) ): # Enable source group @@ -257,7 +263,7 @@ def check_layer_source_tree(layer, smooth_bump_enabled): else: move_mod_groups(layer, layer_tree, source_tree) else: - source_tree = source_group.node_tree + source_tree = get_source_tree(layer, layer_tree) source = source_tree.nodes.get(layer.source) baked_source = source_tree.nodes.get(layer.baked_source) @@ -483,6 +489,7 @@ def disable_mask_source_tree(layer, mask): if mask.group_node != '': layer_tree = get_tree(layer) + Triplanar.unwrap_triplanar_group_node(layer_tree.nodes.get(mask.group_node)) mask_tree = get_mask_tree(mask) source_ref = mask_tree.nodes.get(mask.source) @@ -747,10 +754,11 @@ def check_mask_source_tree(layer, specific_mask=None): #, ch=None): for i, mask in enumerate(layer.masks): if specific_mask and specific_mask != mask: continue - if smooth_bump_ch and get_channel_enabled(smooth_bump_ch, layer, yp.channels[ch_idx]) and get_mask_enabled(mask) and ( + if (get_mask_enabled(mask) and is_entity_using_triplanar(mask)) or ( + smooth_bump_ch and get_channel_enabled(smooth_bump_ch, layer, yp.channels[ch_idx]) and get_mask_enabled(mask) and ( mask.channels[ch_idx].enable and height_process_needed and (write_height_ch or i < chain) and (mask.use_baked or mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER'}) - ): + )): enable_mask_source_tree(layer, mask) else: disable_mask_source_tree(layer, mask) @@ -1578,16 +1586,18 @@ def check_layer_projection_blends(layer): if hasattr(source, 'projection_blend'): source.projection_blend = layer.projection_blend + override_blend = layer.triplanar_blend if layer.texcoord_type == 'Triplanar' else layer.projection_blend + for ch in layer.channels: if ch.override and ch.override_type == 'IMAGE': source = get_channel_source(ch, layer) if hasattr(source, 'projection_blend'): - source.projection_blend = layer.projection_blend + source.projection_blend = override_blend if ch.override_1 and ch.override_1_type == 'IMAGE': source = get_channel_source_1(ch, layer) if hasattr(source, 'projection_blend'): - source.projection_blend = layer.projection_blend + source.projection_blend = override_blend def check_layer_projections(layer): # Set image source projection @@ -1596,14 +1606,15 @@ def check_layer_projections(layer): source.projection = 'BOX' if layer.texcoord_type in {'Generated', 'Object'} else 'FLAT' # Set channel override images + # NOTE: Triplanar uses native box projection for override images since they aren't inside the entity source tree for ch in layer.channels: if ch.override and ch.override_type == 'IMAGE': source = get_channel_source(ch, layer) - source.projection = 'BOX' if layer.texcoord_type in {'Generated', 'Object'} else 'FLAT' + source.projection = 'BOX' if layer.texcoord_type in {'Generated', 'Object', 'Triplanar'} else 'FLAT' if ch.override_1 and ch.override_1_type == 'IMAGE': source = get_channel_source_1(ch, layer) - source.projection = 'BOX' if layer.texcoord_type in {'Generated', 'Object'} else 'FLAT' + source.projection = 'BOX' if layer.texcoord_type in {'Generated', 'Object', 'Triplanar'} else 'FLAT' # Check projection blends check_layer_projection_blends(layer) diff --git a/ui.py b/ui.py index 11d3056e..ce9c9960 100644 --- a/ui.py +++ b/ui.py @@ -1771,6 +1771,11 @@ def draw_layer_vector(context, layout, layer, layer_tree, source, image, vcol, i split = split_layout(rrow, 0.4, align=True) split.prop(layer, 'texcoord_type', text='') split.prop(texcoord, 'object', text='') + elif layer.texcoord_type == 'Triplanar' and not lui.expand_vector: + rrow.scale_x = 0.5 + split = split_layout(rrow, 0.5, align=True) + split.prop(layer, 'texcoord_type', text='') + draw_input_prop(split, layer, 'triplanar_blend', layer=layer) else: rrow.prop(layer, 'texcoord_type', text='') @@ -1853,7 +1858,29 @@ def draw_layer_vector(context, layout, layer, layer_tree, source, image, vcol, i rrow = boxcol.row(align=True) rrow.label(text='', icon='BLANK1') rrow.operator('wm.y_set_decal_object_position_to_sursor', text='Set Position to Cursor', icon='CURSOR') - + + if layer.texcoord_type == 'Triplanar': + rrow = boxcol.row(align=True) + rrow.label(text='', icon='BLANK1') + splits = split_layout(rrow, 0.5, align=True) + splits.label(text='Projection Blend:') + draw_input_prop(splits, layer, 'triplanar_blend', layer=layer) + + rrow = boxcol.row(align=True) + rrow.label(text='', icon='BLANK1') + splits = split_layout(rrow, 0.5, align=True) + splits.label(text='Expand:') + draw_input_prop(splits, layer, 'triplanar_expand', layer=layer) + + for axis in triplanar_axes: + rrow = boxcol.row(align=True) + rrow.label(text='', icon='BLANK1') + splits = split_layout(rrow, 0.5, align=True) + splits.label(text='Show Sides:' if axis == 'X' else '') + rrrow = splits.row(align=True) + draw_input_prop(rrrow, layer, triplanar_side_props['+' + axis], None, '+' + axis, layer=layer) + draw_input_prop(rrrow, layer, triplanar_side_props['-' + axis], None, '-' + axis, layer=layer) + if layer.texcoord_type != 'Decal' and not is_using_image_atlas: mapping = get_layer_mapping(layer) @@ -3161,6 +3188,10 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): if texcoord: ssplit.prop(mask, 'texcoord_type', text='') ssplit.prop(texcoord, 'object', text='') + elif mask.texcoord_type == 'Triplanar' and not maskui.expand_vector: + rrrow = split_layout(rrow, 0.5, align=True) + rrrow.prop(mask, 'texcoord_type', text='') + draw_input_prop(rrrow, mask, 'triplanar_blend', layer=layer) else: rrow.prop(mask, 'texcoord_type', text='') @@ -3222,6 +3253,22 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): else: boxcol.operator('wm.y_select_decal_object', icon='EMPTY_DATA') boxcol.operator('wm.y_set_decal_object_position_to_sursor', text='Set Position to Cursor', icon='CURSOR') + if mask.texcoord_type == 'Triplanar': + splits = split_layout(boxcol, 0.5, align=True) + splits.label(text='Projection Blend:') + draw_input_prop(splits, mask, 'triplanar_blend', layer=layer) + + splits = split_layout(boxcol, 0.5, align=True) + splits.label(text='Expand:') + draw_input_prop(splits, mask, 'triplanar_expand', layer=layer) + + for axis in triplanar_axes: + splits = split_layout(boxcol, 0.5, align=True) + splits.label(text='Show Sides:' if axis == 'X' else '') + rrrow = splits.row(align=True) + draw_input_prop(rrrow, mask, triplanar_side_props['+' + axis], None, '+' + axis, layer=layer) + draw_input_prop(rrrow, mask, triplanar_side_props['-' + axis], None, '-' + axis, layer=layer) + if mask.texcoord_type != 'Decal' and not is_using_image_atlas: mapping = get_mask_mapping(mask)