FBX IO: Export corner normals instead of face normals #105150
@ -32,10 +32,10 @@ if flag is False:
|
|||||||
# Import modules
|
# Import modules
|
||||||
# ----------------------------------------------
|
# ----------------------------------------------
|
||||||
if "bpy" in locals():
|
if "bpy" in locals():
|
||||||
import imp
|
import importlib
|
||||||
|
|
||||||
imp.reload(import_ase)
|
importlib.reload(import_ase)
|
||||||
imp.reload(import_krita)
|
importlib.reload(import_krita)
|
||||||
else:
|
else:
|
||||||
import import_ase
|
import import_ase
|
||||||
import import_krita
|
import import_krita
|
||||||
|
@ -3110,9 +3110,9 @@ def fbx_header_elements(root, scene_data, time=None):
|
|||||||
app_name = "Blender (stable FBX IO)"
|
app_name = "Blender (stable FBX IO)"
|
||||||
app_ver = bpy.app.version_string
|
app_ver = bpy.app.version_string
|
||||||
|
|
||||||
import addon_utils
|
from . import bl_info
|
||||||
import sys
|
addon_ver = bl_info["version"]
|
||||||
addon_ver = addon_utils.module_bl_info(sys.modules[__package__])['version']
|
del bl_info
|
||||||
|
|
||||||
# ##### Start of FBXHeaderExtension element.
|
# ##### Start of FBXHeaderExtension element.
|
||||||
header_ext = elem_empty(root, b"FBXHeaderExtension")
|
header_ext = elem_empty(root, b"FBXHeaderExtension")
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
'name': 'glTF 2.0 format',
|
'name': 'glTF 2.0 format',
|
||||||
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
||||||
"version": (4, 1, 38),
|
"version": (4, 1, 60),
|
||||||
'blender': (4, 1, 0),
|
'blender': (4, 1, 0),
|
||||||
'location': 'File > Import-Export',
|
'location': 'File > Import-Export',
|
||||||
'description': 'Import-Export as glTF 2.0',
|
'description': 'Import-Export as glTF 2.0',
|
||||||
|
@ -139,6 +139,13 @@ def get_attribute_type(component_type, data_type):
|
|||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_attribute(attributes, name, data_type, domain):
|
||||||
|
attribute = attributes.get(name)
|
||||||
|
if attribute is not None and attribute.data_type == data_type and attribute.domain == domain:
|
||||||
|
return attribute
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_gltf_interpolation(interpolation):
|
def get_gltf_interpolation(interpolation):
|
||||||
return {
|
return {
|
||||||
"BEZIER": "CUBICSPLINE",
|
"BEZIER": "CUBICSPLINE",
|
||||||
|
69
io_scene_gltf2/blender/com/gltf2_blender_utils.py
Normal file
69
io_scene_gltf2/blender/com/gltf2_blender_utils.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2018-2024 The glTF-Blender-IO authors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def fast_structured_np_unique(arr, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
np.unique optimized for structured arrays when a sorted result is not required.
|
||||||
|
|
||||||
|
np.unique works through sorting, but sorting a structured array requires as many sorts as there are fields in the
|
||||||
|
structured dtype.
|
||||||
|
|
||||||
|
By viewing the array as a single non-structured dtype that sorts according to its bytes, unique elements can be
|
||||||
|
found with a single sort. Since the values are viewed as a different type to their original, this means that the
|
||||||
|
returned array of unique values may not be sorted according to their original type.
|
||||||
|
|
||||||
|
Float field caveats:
|
||||||
|
All elements of -0.0 in the input array will be replaced with 0.0 to ensure that both values are collapsed into one.
|
||||||
|
NaN values can have lots of different byte representations (e.g. signalling/quiet and custom payloads). Only the
|
||||||
|
duplicates of each unique byte representation will be collapsed into one.
|
||||||
|
|
||||||
|
Nested structured dtypes are not supported.
|
||||||
|
The behaviour of structured dtypes with overlapping fields is undefined.
|
||||||
|
"""
|
||||||
|
structured_dtype = arr.dtype
|
||||||
|
fields = structured_dtype.fields
|
||||||
|
if fields is None:
|
||||||
|
raise RuntimeError('%s is not a structured dtype' % structured_dtype)
|
||||||
|
|
||||||
|
for field_name, (field_dtype, *_offset_and_optional_title) in fields.items():
|
||||||
|
if field_dtype.subdtype is not None:
|
||||||
|
raise RuntimeError('Nested structured types are not supported in %s' % structured_dtype)
|
||||||
|
if field_dtype.kind == 'f':
|
||||||
|
# Replace all -0.0 in the array with 0.0 because -0.0 and 0.0 have different byte representations.
|
||||||
|
arr[field_name][arr[field_name] == -0.0] = 0.0
|
||||||
|
elif field_dtype.kind not in "iuUSV":
|
||||||
|
# Signed integer, unsigned integer, unicode string, byte string (bytes) and raw bytes (void) can be left
|
||||||
|
# as they are. Everything else is unsupported.
|
||||||
|
raise RuntimeError('Unsupported structured field type %s for field %s' % (field_dtype, field_name))
|
||||||
|
|
||||||
|
structured_itemsize = structured_dtype.itemsize
|
||||||
|
|
||||||
|
# Integer types sort the fastest, but are only available for specific itemsizes.
|
||||||
|
uint_dtypes_by_itemsize = {1: np.uint8, 2: np.uint16, 4: np.uint32, 8: np.uint64}
|
||||||
|
# Signed/unsigned makes no noticeable speed difference, but using unsigned will result in ordering according to
|
||||||
|
# individual bytes like the other, non-integer types.
|
||||||
|
if structured_itemsize in uint_dtypes_by_itemsize:
|
||||||
|
entire_structure_dtype = uint_dtypes_by_itemsize[structured_itemsize]
|
||||||
|
else:
|
||||||
|
# Construct a flexible size dtype with matching itemsize to the entire structured dtype.
|
||||||
|
# Should always be 4 because each character in a unicode string is UCS4.
|
||||||
|
str_itemsize = np.dtype((np.str_, 1)).itemsize
|
||||||
|
if structured_itemsize % str_itemsize == 0:
|
||||||
|
# Unicode strings seem to be slightly faster to sort than bytes.
|
||||||
|
entire_structure_dtype = np.dtype((np.str_, structured_itemsize // str_itemsize))
|
||||||
|
else:
|
||||||
|
# Bytes seem to be slightly faster to sort than raw bytes (np.void).
|
||||||
|
entire_structure_dtype = np.dtype((np.bytes_, structured_itemsize))
|
||||||
|
|
||||||
|
result = np.unique(arr.view(entire_structure_dtype), *args, **kwargs)
|
||||||
|
|
||||||
|
unique = result[0] if isinstance(result, tuple) else result
|
||||||
|
# View in the original dtype.
|
||||||
|
unique = unique.view(arr.dtype)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
return (unique,) + result[1:]
|
||||||
|
else:
|
||||||
|
return unique
|
@ -135,7 +135,7 @@ def __convert_keyframes(
|
|||||||
transform = matrix_parent_inverse
|
transform = matrix_parent_inverse
|
||||||
|
|
||||||
values = []
|
values = []
|
||||||
fps = bpy.context.scene.render.fps
|
fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
for keyframe in keyframes:
|
for keyframe in keyframes:
|
||||||
# Transform the data and build gltf control points
|
# Transform the data and build gltf control points
|
||||||
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction)
|
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction)
|
||||||
|
@ -9,9 +9,9 @@ from ...com import gltf2_blender_math
|
|||||||
|
|
||||||
class Keyframe:
|
class Keyframe:
|
||||||
def __init__(self, channels: typing.Tuple[bpy.types.FCurve], frame: float, bake_channel: typing.Union[str, None]):
|
def __init__(self, channels: typing.Tuple[bpy.types.FCurve], frame: float, bake_channel: typing.Union[str, None]):
|
||||||
self.seconds = frame / bpy.context.scene.render.fps
|
self.seconds = frame / (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
self.frame = frame
|
self.frame = frame
|
||||||
self.fps = bpy.context.scene.render.fps
|
self.fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
self.__length_morph = 0
|
self.__length_morph = 0
|
||||||
# Note: channels has some None items only for SK if some SK are not animated
|
# Note: channels has some None items only for SK if some SK are not animated
|
||||||
if bake_channel is None:
|
if bake_channel is None:
|
||||||
|
@ -94,7 +94,7 @@ def __convert_keyframes(armature_uuid, bone_name, channel, keyframes, action_nam
|
|||||||
if armature_uuid in export_settings['slide'].keys() and action_name in export_settings['slide'][armature_uuid].keys():
|
if armature_uuid in export_settings['slide'].keys() and action_name in export_settings['slide'][armature_uuid].keys():
|
||||||
for k in keyframes:
|
for k in keyframes:
|
||||||
k.frame += -export_settings['slide'][armature_uuid][action_name]
|
k.frame += -export_settings['slide'][armature_uuid][action_name]
|
||||||
k.seconds = k.frame / bpy.context.scene.render.fps
|
k.seconds = k.frame / (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
|
|
||||||
times = [k.seconds for k in keyframes]
|
times = [k.seconds for k in keyframes]
|
||||||
input = gather_accessor(
|
input = gather_accessor(
|
||||||
@ -145,7 +145,7 @@ def __convert_keyframes(armature_uuid, bone_name, channel, keyframes, action_nam
|
|||||||
transform = correction_matrix_local
|
transform = correction_matrix_local
|
||||||
|
|
||||||
values = []
|
values = []
|
||||||
fps = bpy.context.scene.render.fps
|
fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
for keyframe in keyframes:
|
for keyframe in keyframes:
|
||||||
# Transform the data and build gltf control points
|
# Transform the data and build gltf control points
|
||||||
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, False)
|
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, False)
|
||||||
@ -206,6 +206,8 @@ def __gather_interpolation(node_channel_is_animated, node_channel_interpolation,
|
|||||||
elif len(keyframes) == 1:
|
elif len(keyframes) == 1:
|
||||||
if node_channel_is_animated is False:
|
if node_channel_is_animated is False:
|
||||||
return "STEP"
|
return "STEP"
|
||||||
|
elif node_channel_interpolation == "CUBICSPLINE":
|
||||||
|
return "LINEAR" # We can't have a single keyframe with CUBICSPLINE
|
||||||
else:
|
else:
|
||||||
return node_channel_interpolation
|
return node_channel_interpolation
|
||||||
else:
|
else:
|
||||||
|
@ -78,7 +78,7 @@ def __convert_keyframes(obj_uuid: str, channel: str, keyframes, action_name: str
|
|||||||
if obj_uuid in export_settings['slide'].keys() and action_name in export_settings['slide'][obj_uuid].keys():
|
if obj_uuid in export_settings['slide'].keys() and action_name in export_settings['slide'][obj_uuid].keys():
|
||||||
for k in keyframes:
|
for k in keyframes:
|
||||||
k.frame += -export_settings['slide'][obj_uuid][action_name]
|
k.frame += -export_settings['slide'][obj_uuid][action_name]
|
||||||
k.seconds = k.frame / bpy.context.scene.render.fps
|
k.seconds = k.frame / (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
|
|
||||||
times = [k.seconds for k in keyframes]
|
times = [k.seconds for k in keyframes]
|
||||||
input = gather_accessor(
|
input = gather_accessor(
|
||||||
@ -100,7 +100,7 @@ def __convert_keyframes(obj_uuid: str, channel: str, keyframes, action_name: str
|
|||||||
|
|
||||||
|
|
||||||
values = []
|
values = []
|
||||||
fps = bpy.context.scene.render.fps
|
fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
for keyframe in keyframes:
|
for keyframe in keyframes:
|
||||||
|
|
||||||
# Transform the data and build gltf control points
|
# Transform the data and build gltf control points
|
||||||
@ -148,6 +148,8 @@ def __gather_interpolation(
|
|||||||
elif len(keyframes) == 1:
|
elif len(keyframes) == 1:
|
||||||
if node_channel_is_animated is False:
|
if node_channel_is_animated is False:
|
||||||
return "STEP"
|
return "STEP"
|
||||||
|
elif node_channel_interpolation == "CUBICSPLINE":
|
||||||
|
return "LINEAR" # We can't have a single keyframe with CUBICSPLINE
|
||||||
else:
|
else:
|
||||||
return node_channel_interpolation
|
return node_channel_interpolation
|
||||||
else:
|
else:
|
||||||
|
@ -66,7 +66,7 @@ def __convert_keyframes(obj_uuid, keyframes, action_name: str, export_settings):
|
|||||||
if obj_uuid in export_settings['slide'].keys() and action_name in export_settings['slide'][obj_uuid].keys():
|
if obj_uuid in export_settings['slide'].keys() and action_name in export_settings['slide'][obj_uuid].keys():
|
||||||
for k in keyframes:
|
for k in keyframes:
|
||||||
k.frame += -export_settings['slide'][obj_uuid][action_name]
|
k.frame += -export_settings['slide'][obj_uuid][action_name]
|
||||||
k.seconds = k.frame / bpy.context.scene.render.fps
|
k.seconds = k.frame / (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
|
|
||||||
times = [k.seconds for k in keyframes]
|
times = [k.seconds for k in keyframes]
|
||||||
input = gather_accessor(
|
input = gather_accessor(
|
||||||
|
@ -29,13 +29,27 @@ def gather_node(vnode, export_settings):
|
|||||||
if skin is not None:
|
if skin is not None:
|
||||||
vnode.skin = skin
|
vnode.skin = skin
|
||||||
|
|
||||||
|
|
||||||
|
# Hook to check if we should export mesh or not (force it to None)
|
||||||
|
class GltfHookNodeMesh:
|
||||||
|
def __init__(self):
|
||||||
|
self.export_mesh = True
|
||||||
|
|
||||||
|
gltf_hook_node_mesh = GltfHookNodeMesh()
|
||||||
|
|
||||||
|
export_user_extensions('gather_node_mesh_hook', export_settings, gltf_hook_node_mesh, blender_object)
|
||||||
|
if gltf_hook_node_mesh.export_mesh is True:
|
||||||
|
mesh = __gather_mesh(vnode, blender_object, export_settings)
|
||||||
|
else:
|
||||||
|
mesh = None
|
||||||
|
|
||||||
node = gltf2_io.Node(
|
node = gltf2_io.Node(
|
||||||
camera=__gather_camera(vnode, export_settings),
|
camera=__gather_camera(vnode, export_settings),
|
||||||
children=__gather_children(vnode, export_settings),
|
children=__gather_children(vnode, export_settings),
|
||||||
extensions=__gather_extensions(vnode, export_settings),
|
extensions=__gather_extensions(vnode, export_settings),
|
||||||
extras=__gather_extras(blender_object, export_settings),
|
extras=__gather_extras(blender_object, export_settings),
|
||||||
matrix=__gather_matrix(blender_object, export_settings),
|
matrix=__gather_matrix(blender_object, export_settings),
|
||||||
mesh=__gather_mesh(vnode, blender_object, export_settings),
|
mesh=mesh,
|
||||||
name=__gather_name(blender_object, export_settings),
|
name=__gather_name(blender_object, export_settings),
|
||||||
rotation=None,
|
rotation=None,
|
||||||
scale=None,
|
scale=None,
|
||||||
|
@ -11,6 +11,7 @@ from ...io.com.gltf2_io_constants import ROUNDING_DIGIT
|
|||||||
from ...io.exp.gltf2_io_user_extensions import export_user_extensions
|
from ...io.exp.gltf2_io_user_extensions import export_user_extensions
|
||||||
from ...io.com import gltf2_io_constants
|
from ...io.com import gltf2_io_constants
|
||||||
from ..com import gltf2_blender_conversion
|
from ..com import gltf2_blender_conversion
|
||||||
|
from ..com.gltf2_blender_utils import fast_structured_np_unique
|
||||||
from .material.gltf2_blender_gather_materials import get_base_material, get_material_from_idx, get_active_uvmap_index, get_new_material_texture_shared
|
from .material.gltf2_blender_gather_materials import get_base_material, get_material_from_idx, get_active_uvmap_index, get_new_material_texture_shared
|
||||||
from .material.gltf2_blender_gather_texture_info import gather_udim_texture_info
|
from .material.gltf2_blender_gather_texture_info import gather_udim_texture_info
|
||||||
from . import gltf2_blender_gather_skins
|
from . import gltf2_blender_gather_skins
|
||||||
@ -334,10 +335,12 @@ class PrimitiveCreator:
|
|||||||
|
|
||||||
|
|
||||||
def populate_dots_data(self):
|
def populate_dots_data(self):
|
||||||
vidxs = np.empty(len(self.blender_mesh.loops))
|
corner_vertex_indices = gltf2_blender_conversion.get_attribute(self.blender_mesh.attributes, '.corner_vert', 'INT', 'CORNER')
|
||||||
self.blender_mesh.loops.foreach_get('vertex_index', vidxs)
|
if corner_vertex_indices:
|
||||||
self.dots['vertex_index'] = vidxs
|
vidxs = np.empty(len(self.blender_mesh.loops), dtype=np.intc)
|
||||||
del vidxs
|
corner_vertex_indices.data.foreach_get('value', vidxs)
|
||||||
|
self.dots['vertex_index'] = vidxs
|
||||||
|
del vidxs
|
||||||
|
|
||||||
for attr in self.blender_attributes:
|
for attr in self.blender_attributes:
|
||||||
if 'skip_getting_to_dots' in attr:
|
if 'skip_getting_to_dots' in attr:
|
||||||
@ -412,11 +415,19 @@ class PrimitiveCreator:
|
|||||||
# Now we need to get data and populate
|
# Now we need to get data and populate
|
||||||
for attr in self.uvmap_attribute_list:
|
for attr in self.uvmap_attribute_list:
|
||||||
if attr + str(0) not in self.dots.dtype.names: # In case user exports custom attributes, we may have it already
|
if attr + str(0) not in self.dots.dtype.names: # In case user exports custom attributes, we may have it already
|
||||||
# Vector in custom Attributes are Vector3, but keeping only the first two data
|
# Vector in custom Attributes are Vector2 or Vector3 (but keeping only the first two data)
|
||||||
data = np.empty(len(self.blender_mesh.loops) * 3, gltf2_blender_conversion.get_numpy_type('FLOAT2'))
|
if self.blender_mesh.attributes[attr].data_type == "FLOAT_VECTOR":
|
||||||
self.blender_mesh.attributes[attr].data.foreach_get('vector', data)
|
data = np.empty(len(self.blender_mesh.loops) * 3, gltf2_blender_conversion.get_numpy_type('FLOAT2'))
|
||||||
data = data.reshape(-1, 3)
|
self.blender_mesh.attributes[attr].data.foreach_get('vector', data)
|
||||||
data = data[:,:2]
|
data = data.reshape(-1, 3)
|
||||||
|
data = data[:,:2]
|
||||||
|
elif self.blender_mesh.attributes[attr].data_type == "FLOAT2":
|
||||||
|
data = np.empty(len(self.blender_mesh.loops) * 2, gltf2_blender_conversion.get_numpy_type('FLOAT2'))
|
||||||
|
self.blender_mesh.attributes[attr].data.foreach_get('vector', data)
|
||||||
|
data = data.reshape(-1, 2)
|
||||||
|
else:
|
||||||
|
print_console('WARNING', 'We are not managing this case yet (UVMap as custom attribute for unknown type)')
|
||||||
|
continue
|
||||||
# Blender UV space -> glTF UV space
|
# Blender UV space -> glTF UV space
|
||||||
# u,v -> u,1-v
|
# u,v -> u,1-v
|
||||||
data[:, 1] *= -1
|
data[:, 1] *= -1
|
||||||
@ -621,7 +632,7 @@ class PrimitiveCreator:
|
|||||||
|
|
||||||
def primitive_creation_shared(self):
|
def primitive_creation_shared(self):
|
||||||
primitives = []
|
primitives = []
|
||||||
self.dots, shared_dot_indices = np.unique(self.dots, return_inverse=True)
|
self.dots, shared_dot_indices = fast_structured_np_unique(self.dots, return_inverse=True)
|
||||||
|
|
||||||
self.blender_idxs = self.dots['vertex_index']
|
self.blender_idxs = self.dots['vertex_index']
|
||||||
|
|
||||||
@ -696,7 +707,7 @@ class PrimitiveCreator:
|
|||||||
# Extract just dots used by this primitive, deduplicate them, and
|
# Extract just dots used by this primitive, deduplicate them, and
|
||||||
# calculate indices into this deduplicated list.
|
# calculate indices into this deduplicated list.
|
||||||
self.prim_dots = self.dots[dot_indices]
|
self.prim_dots = self.dots[dot_indices]
|
||||||
self.prim_dots, indices = np.unique(self.prim_dots, return_inverse=True)
|
self.prim_dots, indices = fast_structured_np_unique(self.prim_dots, return_inverse=True)
|
||||||
|
|
||||||
if len(self.prim_dots) == 0:
|
if len(self.prim_dots) == 0:
|
||||||
continue
|
continue
|
||||||
@ -770,7 +781,7 @@ class PrimitiveCreator:
|
|||||||
if self.blender_idxs_edges.shape[0] > 0:
|
if self.blender_idxs_edges.shape[0] > 0:
|
||||||
# Export one glTF vert per unique Blender vert in a loose edge
|
# Export one glTF vert per unique Blender vert in a loose edge
|
||||||
self.blender_idxs = self.blender_idxs_edges
|
self.blender_idxs = self.blender_idxs_edges
|
||||||
dots_edges, indices = np.unique(self.dots_edges, return_inverse=True)
|
dots_edges, indices = fast_structured_np_unique(self.dots_edges, return_inverse=True)
|
||||||
self.blender_idxs = np.unique(self.blender_idxs_edges)
|
self.blender_idxs = np.unique(self.blender_idxs_edges)
|
||||||
|
|
||||||
self.attributes_edges_points = {}
|
self.attributes_edges_points = {}
|
||||||
@ -871,14 +882,21 @@ class PrimitiveCreator:
|
|||||||
|
|
||||||
def __get_positions(self):
|
def __get_positions(self):
|
||||||
self.locs = np.empty(len(self.blender_mesh.vertices) * 3, dtype=np.float32)
|
self.locs = np.empty(len(self.blender_mesh.vertices) * 3, dtype=np.float32)
|
||||||
source = self.key_blocks[0].relative_key.data if self.key_blocks else self.blender_mesh.vertices
|
if self.key_blocks:
|
||||||
source.foreach_get('co', self.locs)
|
source = self.key_blocks[0].relative_key.points
|
||||||
|
foreach_attribute = 'co'
|
||||||
|
else:
|
||||||
|
position_attribute = gltf2_blender_conversion.get_attribute(self.blender_mesh.attributes, 'position', 'FLOAT_VECTOR', 'POINT')
|
||||||
|
source = position_attribute.data if position_attribute else None
|
||||||
|
foreach_attribute = 'vector'
|
||||||
|
if source:
|
||||||
|
source.foreach_get(foreach_attribute, self.locs)
|
||||||
self.locs = self.locs.reshape(len(self.blender_mesh.vertices), 3)
|
self.locs = self.locs.reshape(len(self.blender_mesh.vertices), 3)
|
||||||
|
|
||||||
self.morph_locs = []
|
self.morph_locs = []
|
||||||
for key_block in self.key_blocks:
|
for key_block in self.key_blocks:
|
||||||
vs = np.empty(len(self.blender_mesh.vertices) * 3, dtype=np.float32)
|
vs = np.empty(len(self.blender_mesh.vertices) * 3, dtype=np.float32)
|
||||||
key_block.data.foreach_get('co', vs)
|
key_block.points.foreach_get('co', vs)
|
||||||
vs = vs.reshape(len(self.blender_mesh.vertices), 3)
|
vs = vs.reshape(len(self.blender_mesh.vertices), 3)
|
||||||
self.morph_locs.append(vs)
|
self.morph_locs.append(vs)
|
||||||
|
|
||||||
@ -1116,7 +1134,7 @@ class PrimitiveCreator:
|
|||||||
def __get_uvs_attribute(self, blender_uv_idx, attr):
|
def __get_uvs_attribute(self, blender_uv_idx, attr):
|
||||||
layer = self.blender_mesh.uv_layers[blender_uv_idx]
|
layer = self.blender_mesh.uv_layers[blender_uv_idx]
|
||||||
uvs = np.empty(len(self.blender_mesh.loops) * 2, dtype=np.float32)
|
uvs = np.empty(len(self.blender_mesh.loops) * 2, dtype=np.float32)
|
||||||
layer.data.foreach_get('uv', uvs)
|
layer.uv.foreach_get('vector', uvs)
|
||||||
uvs = uvs.reshape(len(self.blender_mesh.loops), 2)
|
uvs = uvs.reshape(len(self.blender_mesh.loops), 2)
|
||||||
|
|
||||||
# Blender UV space -> glTF UV space
|
# Blender UV space -> glTF UV space
|
||||||
@ -1136,7 +1154,7 @@ class PrimitiveCreator:
|
|||||||
self.normals = np.array(self.normals, dtype=np.float32)
|
self.normals = np.array(self.normals, dtype=np.float32)
|
||||||
else:
|
else:
|
||||||
self.normals = np.empty(len(self.blender_mesh.loops) * 3, dtype=np.float32)
|
self.normals = np.empty(len(self.blender_mesh.loops) * 3, dtype=np.float32)
|
||||||
self.blender_mesh.loops.foreach_get('normal', self.normals)
|
self.blender_mesh.corner_normals.foreach_get('vector', self.normals)
|
||||||
|
|
||||||
self.normals = self.normals.reshape(len(self.blender_mesh.loops), 3)
|
self.normals = self.normals.reshape(len(self.blender_mesh.loops), 3)
|
||||||
|
|
||||||
|
@ -568,6 +568,10 @@ class VExportTree:
|
|||||||
hasattr(self.nodes[n.armature], "need_neutral_bone")]: #all skin meshes objects where neutral bone is needed
|
hasattr(self.nodes[n.armature], "need_neutral_bone")]: #all skin meshes objects where neutral bone is needed
|
||||||
# Only for meshes, as curve can't have skin data (no weights pain available)
|
# Only for meshes, as curve can't have skin data (no weights pain available)
|
||||||
|
|
||||||
|
# Be sure to add it to really exported meshes
|
||||||
|
if n.node.skin is None:
|
||||||
|
print("WARNING: {} has no skin, skipping adding neutral bone data on it.".format(n.blender_object.name))
|
||||||
|
continue
|
||||||
|
|
||||||
if n.armature not in added_armatures:
|
if n.armature not in added_armatures:
|
||||||
|
|
||||||
|
@ -37,8 +37,11 @@ def get_material_cache_key(blender_material, export_settings):
|
|||||||
# Use id of material
|
# Use id of material
|
||||||
# Do not use bpy.types that can be unhashable
|
# Do not use bpy.types that can be unhashable
|
||||||
# Do not use material name, that can be not unique (when linked)
|
# Do not use material name, that can be not unique (when linked)
|
||||||
|
# We use here the id of original material as for apply modifier, the material has a new id
|
||||||
|
# So, when no modifier applied => original is the same id
|
||||||
|
# And when modifier applied => new one is different id, but original is still the same
|
||||||
return (
|
return (
|
||||||
(id(blender_material),),
|
(id(blender_material.original),),
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_by_key(key=get_material_cache_key)
|
@cached_by_key(key=get_material_cache_key)
|
||||||
|
@ -144,8 +144,10 @@ def __gather_metallic_roughness_texture(blender_material, orm_texture, export_se
|
|||||||
# Using directlty the Blender socket object
|
# Using directlty the Blender socket object
|
||||||
if not hasMetal and not hasRough:
|
if not hasMetal and not hasRough:
|
||||||
metallic_roughness = get_socket_from_gltf_material_node(blender_material, "MetallicRoughness")
|
metallic_roughness = get_socket_from_gltf_material_node(blender_material, "MetallicRoughness")
|
||||||
if metallic_roughness is None or not has_image_node_from_socket(metallic_roughness, export_settings):
|
if metallic_roughness.socket is None or not has_image_node_from_socket(metallic_roughness, export_settings):
|
||||||
return None, {}, {}, None
|
return None, {}, {}, None
|
||||||
|
else:
|
||||||
|
texture_input = (metallic_roughness, metallic_roughness)
|
||||||
elif not hasMetal:
|
elif not hasMetal:
|
||||||
texture_input = (roughness_socket,)
|
texture_input = (roughness_socket,)
|
||||||
elif not hasRough:
|
elif not hasRough:
|
||||||
@ -154,6 +156,7 @@ def __gather_metallic_roughness_texture(blender_material, orm_texture, export_se
|
|||||||
texture_input = (metallic_socket, roughness_socket)
|
texture_input = (metallic_socket, roughness_socket)
|
||||||
|
|
||||||
tex, uvmap_info, udim_info, factor = gather_texture_info(
|
tex, uvmap_info, udim_info, factor = gather_texture_info(
|
||||||
|
|
||||||
texture_input[0],
|
texture_input[0],
|
||||||
orm_texture or texture_input,
|
orm_texture or texture_input,
|
||||||
export_settings,
|
export_settings,
|
||||||
|
@ -177,7 +177,7 @@ def get_socket_from_gltf_material_node(blender_material: bpy.types.Material, nam
|
|||||||
if blender_material.node_tree and blender_material.use_nodes:
|
if blender_material.node_tree and blender_material.use_nodes:
|
||||||
nodes = get_material_nodes(blender_material.node_tree, [blender_material], bpy.types.ShaderNodeGroup)
|
nodes = get_material_nodes(blender_material.node_tree, [blender_material], bpy.types.ShaderNodeGroup)
|
||||||
# Some weird node groups with missing datablock can have no node_tree, so checking n.node_tree (See #1797)
|
# Some weird node groups with missing datablock can have no node_tree, so checking n.node_tree (See #1797)
|
||||||
nodes = [n for n in nodes if n[0].node_tree is not None and ( n[0].node_tree.name.lower().startswith(get_gltf_old_group_node_name()) or n[0].node_tree.name.lower() in gltf_node_group_names)]
|
nodes = [n for n in nodes if n[0].node_tree is not None and any([[n[0].node_tree.name.lower().startswith(g) for g in gltf_node_group_names]])]
|
||||||
inputs = sum([[(input, node[1]) for input in node[0].inputs if input.name == name] for node in nodes], [])
|
inputs = sum([[(input, node[1]) for input in node[0].inputs if input.name == name] for node in nodes], [])
|
||||||
if inputs:
|
if inputs:
|
||||||
return NodeSocket(inputs[0][0], inputs[0][1])
|
return NodeSocket(inputs[0][0], inputs[0][1])
|
||||||
@ -461,6 +461,12 @@ def get_vertex_color_info(color_socket, alpha_socket, export_settings):
|
|||||||
attribute_color_type = "active"
|
attribute_color_type = "active"
|
||||||
elif use_vc is True and use_active is None and attribute_color is not None:
|
elif use_vc is True and use_active is None and attribute_color is not None:
|
||||||
attribute_color_type = "name"
|
attribute_color_type = "name"
|
||||||
|
elif node.node.type in ["ATTRIBUTE", "VERTEX_COLOR"]:
|
||||||
|
use_vc, attribute_color, use_active = get_attribute_name(NodeSocket(node.node.outputs[0], node.group_path), export_settings)
|
||||||
|
if use_vc is True and use_active is True:
|
||||||
|
attribute_color_type = "active"
|
||||||
|
elif use_vc is True and use_active is None and attribute_color is not None:
|
||||||
|
attribute_color_type = "name"
|
||||||
|
|
||||||
if alpha_socket is not None and alpha_socket.socket is not None:
|
if alpha_socket is not None and alpha_socket.socket is not None:
|
||||||
node = previous_node(alpha_socket)
|
node = previous_node(alpha_socket)
|
||||||
@ -473,6 +479,12 @@ def get_vertex_color_info(color_socket, alpha_socket, export_settings):
|
|||||||
attribute_alpha_type = "active"
|
attribute_alpha_type = "active"
|
||||||
elif use_vc is True and use_active is None and attribute_alpha is not None:
|
elif use_vc is True and use_active is None and attribute_alpha is not None:
|
||||||
attribute_alpha_type = "name"
|
attribute_alpha_type = "name"
|
||||||
|
elif node.node.type in ["ATTRIBUTE", "VERTEX_COLOR"]:
|
||||||
|
use_vc, attribute_color, use_active = get_attribute_name(NodeSocket(node.node.outputs[0], node.group_path), export_settings)
|
||||||
|
if use_vc is True and use_active is True:
|
||||||
|
attribute_color_type = "active"
|
||||||
|
elif use_vc is True and use_active is None and attribute_color is not None:
|
||||||
|
attribute_color_type = "name"
|
||||||
|
|
||||||
return {"color": attribute_color, "alpha": attribute_alpha, "color_type": attribute_color_type, "alpha_type": attribute_alpha_type}
|
return {"color": attribute_color, "alpha": attribute_alpha, "color_type": attribute_color_type, "alpha_type": attribute_alpha_type}
|
||||||
|
|
||||||
|
@ -1,157 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2018-2021 The glTF-Blender-IO authors
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
from ...io.com.gltf2_io import TextureInfo, MaterialNormalTextureInfoClass
|
|
||||||
from .gltf2_blender_texture import texture
|
|
||||||
|
|
||||||
|
|
||||||
# [Texture] => [Separate R] => [Clearcoat Factor] =>
|
|
||||||
def clearcoat(mh, location, clearcoat_socket):
|
|
||||||
x, y = location
|
|
||||||
try:
|
|
||||||
ext = mh.pymat.extensions['KHR_materials_clearcoat']
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
clearcoat_factor = ext.get('clearcoatFactor', 0)
|
|
||||||
tex_info = ext.get('clearcoatTexture')
|
|
||||||
if tex_info is not None:
|
|
||||||
tex_info = TextureInfo.from_dict(tex_info)
|
|
||||||
|
|
||||||
if clearcoat_socket is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if tex_info is None:
|
|
||||||
clearcoat_socket.default_value = clearcoat_factor
|
|
||||||
return
|
|
||||||
|
|
||||||
# Mix clearcoat factor
|
|
||||||
if clearcoat_factor != 1:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
|
||||||
node.label = 'Clearcoat Factor'
|
|
||||||
node.location = x - 140, y
|
|
||||||
node.operation = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(clearcoat_socket, node.outputs[0])
|
|
||||||
# Inputs
|
|
||||||
clearcoat_socket = node.inputs[0]
|
|
||||||
node.inputs[1].default_value = clearcoat_factor
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
# Separate RGB
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeSeparateColor')
|
|
||||||
node.location = x - 150, y - 75
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(clearcoat_socket, node.outputs['Red'])
|
|
||||||
# Inputs
|
|
||||||
clearcoat_socket = node.inputs[0]
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
|
||||||
tex_info=tex_info,
|
|
||||||
label='CLEARCOAT',
|
|
||||||
location=(x, y),
|
|
||||||
is_data=True,
|
|
||||||
color_socket=clearcoat_socket,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# [Texture] => [Separate G] => [Roughness Factor] =>
|
|
||||||
def clearcoat_roughness(mh, location, roughness_socket):
|
|
||||||
x, y = location
|
|
||||||
try:
|
|
||||||
ext = mh.pymat.extensions['KHR_materials_clearcoat']
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
roughness_factor = ext.get('clearcoatRoughnessFactor', 0)
|
|
||||||
tex_info = ext.get('clearcoatRoughnessTexture')
|
|
||||||
if tex_info is not None:
|
|
||||||
tex_info = TextureInfo.from_dict(tex_info)
|
|
||||||
|
|
||||||
if roughness_socket is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if tex_info is None:
|
|
||||||
roughness_socket.default_value = roughness_factor
|
|
||||||
return
|
|
||||||
|
|
||||||
# Mix roughness factor
|
|
||||||
if roughness_factor != 1:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
|
||||||
node.label = 'Clearcoat Roughness Factor'
|
|
||||||
node.location = x - 140, y
|
|
||||||
node.operation = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(roughness_socket, node.outputs[0])
|
|
||||||
# Inputs
|
|
||||||
roughness_socket = node.inputs[0]
|
|
||||||
node.inputs[1].default_value = roughness_factor
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
# Separate RGB (roughness is in G)
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeSeparateColor')
|
|
||||||
node.location = x - 150, y - 75
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(roughness_socket, node.outputs['Green'])
|
|
||||||
# Inputs
|
|
||||||
color_socket = node.inputs[0]
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
|
||||||
tex_info=tex_info,
|
|
||||||
label='CLEARCOAT ROUGHNESS',
|
|
||||||
location=(x, y),
|
|
||||||
is_data=True,
|
|
||||||
color_socket=color_socket,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# [Texture] => [Normal Map] =>
|
|
||||||
def clearcoat_normal(mh, location, normal_socket):
|
|
||||||
x,y = location
|
|
||||||
try:
|
|
||||||
ext = mh.pymat.extensions['KHR_materials_clearcoat']
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
tex_info = ext.get('clearcoatNormalTexture')
|
|
||||||
if tex_info is not None:
|
|
||||||
tex_info = MaterialNormalTextureInfoClass.from_dict(tex_info)
|
|
||||||
|
|
||||||
if tex_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Normal map
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeNormalMap')
|
|
||||||
node.location = x - 150, y - 40
|
|
||||||
# Set UVMap
|
|
||||||
uv_idx = tex_info.tex_coord or 0
|
|
||||||
try:
|
|
||||||
uv_idx = tex_info.extensions['KHR_texture_transform']['texCoord']
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
node.uv_map = 'UVMap' if uv_idx == 0 else 'UVMap.%03d' % uv_idx
|
|
||||||
# Set strength
|
|
||||||
scale = tex_info.scale
|
|
||||||
scale = scale if scale is not None else 1
|
|
||||||
node.inputs['Strength'].default_value = scale
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(normal_socket, node.outputs['Normal'])
|
|
||||||
# Inputs
|
|
||||||
color_socket = node.inputs['Color']
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
|
||||||
tex_info=tex_info,
|
|
||||||
label='CLEARCOAT NORMAL',
|
|
||||||
location=(x, y),
|
|
||||||
is_data=True,
|
|
||||||
color_socket=color_socket,
|
|
||||||
)
|
|
@ -1,13 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2018-2021 The glTF-Blender-IO authors
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
from ...io.com.gltf2_io_constants import GLTF_IOR
|
|
||||||
|
|
||||||
def ior(mh, ior_socket):
|
|
||||||
try:
|
|
||||||
ext = mh.pymat.extensions['KHR_materials_ior']
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
ior = ext.get('ior', GLTF_IOR)
|
|
||||||
ior_socket.default_value = ior
|
|
@ -2,159 +2,202 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import bpy
|
||||||
from ...io.com.gltf2_io import TextureInfo
|
from ...io.com.gltf2_io import TextureInfo
|
||||||
from .gltf2_blender_pbrMetallicRoughness import \
|
from .gltf2_blender_pbrMetallicRoughness import \
|
||||||
base_color, emission, normal, occlusion, make_output_nodes, make_settings_node
|
base_color, emission, normal, occlusion, make_settings_node
|
||||||
from .gltf2_blender_texture import texture
|
from .gltf2_blender_material_utils import color_factor_and_texture
|
||||||
|
from .gltf2_blender_texture import texture, get_source
|
||||||
|
from .gltf2_blender_image import BlenderImage
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
def pbr_specular_glossiness(mh):
|
def pbr_specular_glossiness(mh):
|
||||||
"""Creates node tree for pbrSpecularGlossiness materials."""
|
"""Creates node tree for pbrSpecularGlossiness materials."""
|
||||||
# This does option #1 from
|
ext = mh.get_ext('KHR_materials_pbrSpecularGlossiness', {})
|
||||||
# https://github.com/KhronosGroup/glTF-Blender-IO/issues/303
|
|
||||||
|
|
||||||
# Sum a Glossy and Diffuse Shader
|
pbr_node = mh.nodes.new('ShaderNodeBsdfPrincipled')
|
||||||
glossy_node = mh.node_tree.nodes.new('ShaderNodeBsdfGlossy')
|
out_node = mh.nodes.new('ShaderNodeOutputMaterial')
|
||||||
diffuse_node = mh.node_tree.nodes.new('ShaderNodeBsdfDiffuse')
|
pbr_node.location = 10, 300
|
||||||
add_node = mh.node_tree.nodes.new('ShaderNodeAddShader')
|
out_node.location = 300, 300
|
||||||
glossy_node.location = 10, 220
|
mh.links.new(pbr_node.outputs[0], out_node.inputs[0])
|
||||||
diffuse_node.location = 10, 0
|
|
||||||
add_node.location = 230, 100
|
|
||||||
mh.node_tree.links.new(add_node.inputs[0], glossy_node.outputs[0])
|
|
||||||
mh.node_tree.links.new(add_node.inputs[1], diffuse_node.outputs[0])
|
|
||||||
|
|
||||||
emission_socket, alpha_socket, _ = make_output_nodes(
|
locs = calc_locations(mh, ext)
|
||||||
mh,
|
|
||||||
location=(370, 250),
|
|
||||||
additional_location=None, #No additional location needed for SpecGloss
|
|
||||||
shader_socket=add_node.outputs[0],
|
|
||||||
make_emission_socket=mh.needs_emissive(),
|
|
||||||
make_alpha_socket=not mh.is_opaque(),
|
|
||||||
make_volume_socket=None # No possible to have KHR_materials_volume with specular/glossiness
|
|
||||||
)
|
|
||||||
|
|
||||||
if emission_socket:
|
|
||||||
emission(
|
|
||||||
mh,
|
|
||||||
location=(-200, 860),
|
|
||||||
color_socket=emission_socket,
|
|
||||||
strength_socket=emission_socket.node.inputs['Strength']
|
|
||||||
)
|
|
||||||
|
|
||||||
base_color(
|
base_color(
|
||||||
mh,
|
mh,
|
||||||
is_diffuse=True,
|
is_diffuse=True,
|
||||||
location=(-200, 380),
|
location=locs['diffuse'],
|
||||||
color_socket=diffuse_node.inputs['Color'],
|
color_socket=pbr_node.inputs['Base Color'],
|
||||||
alpha_socket=alpha_socket,
|
alpha_socket=pbr_node.inputs['Alpha'] if not mh.is_opaque() else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
specular_glossiness(
|
emission(
|
||||||
mh,
|
mh,
|
||||||
location=(-200, -100),
|
location=locs['emission'],
|
||||||
specular_socket=glossy_node.inputs['Color'],
|
color_socket=pbr_node.inputs['Emission Color'],
|
||||||
roughness_socket=glossy_node.inputs['Roughness'],
|
strength_socket=pbr_node.inputs['Emission Strength'],
|
||||||
)
|
|
||||||
copy_socket(
|
|
||||||
mh,
|
|
||||||
copy_from=glossy_node.inputs['Roughness'],
|
|
||||||
copy_to=diffuse_node.inputs['Roughness'],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
normal(
|
normal(
|
||||||
mh,
|
mh,
|
||||||
location=(-200, -580),
|
location=locs['normal'],
|
||||||
normal_socket=glossy_node.inputs['Normal'],
|
normal_socket=pbr_node.inputs['Normal'],
|
||||||
)
|
|
||||||
copy_socket(
|
|
||||||
mh,
|
|
||||||
copy_from=glossy_node.inputs['Normal'],
|
|
||||||
copy_to=diffuse_node.inputs['Normal'],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if mh.pymat.occlusion_texture is not None:
|
if mh.pymat.occlusion_texture is not None:
|
||||||
if mh.settings_node is None:
|
if mh.settings_node is None:
|
||||||
mh.settings_node = make_settings_node(mh)
|
mh.settings_node = make_settings_node(mh)
|
||||||
mh.settings_node.location = (610, -1060)
|
mh.settings_node.location = 10, 425
|
||||||
|
mh.settings_node.width = 240
|
||||||
occlusion(
|
occlusion(
|
||||||
mh,
|
mh,
|
||||||
location=(510, -970),
|
location=locs['occlusion'],
|
||||||
occlusion_socket=mh.settings_node.inputs['Occlusion'],
|
occlusion_socket=mh.settings_node.inputs['Occlusion'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# The F0 color is the specular tint modulated by
|
||||||
|
# ((1-IOR)/(1+IOR))^2. Setting IOR=1000 makes this factor
|
||||||
|
# approximately 1.
|
||||||
|
pbr_node.inputs['IOR'].default_value = 1000
|
||||||
|
|
||||||
# [Texture] => [Spec/Gloss Factor] => [Gloss to Rough] =>
|
# Specular
|
||||||
def specular_glossiness(mh, location, specular_socket, roughness_socket):
|
color_factor_and_texture(
|
||||||
x, y = location
|
|
||||||
spec_factor = mh.pymat.extensions \
|
|
||||||
['KHR_materials_pbrSpecularGlossiness'] \
|
|
||||||
.get('specularFactor', [1, 1, 1])
|
|
||||||
gloss_factor = mh.pymat.extensions \
|
|
||||||
['KHR_materials_pbrSpecularGlossiness'] \
|
|
||||||
.get('glossinessFactor', 1)
|
|
||||||
spec_gloss_texture = mh.pymat.extensions \
|
|
||||||
['KHR_materials_pbrSpecularGlossiness'] \
|
|
||||||
.get('specularGlossinessTexture', None)
|
|
||||||
if spec_gloss_texture is not None:
|
|
||||||
spec_gloss_texture = TextureInfo.from_dict(spec_gloss_texture)
|
|
||||||
|
|
||||||
if spec_gloss_texture is None:
|
|
||||||
specular_socket.default_value = spec_factor + [1]
|
|
||||||
roughness_socket.default_value = 1 - gloss_factor
|
|
||||||
return
|
|
||||||
|
|
||||||
# (1 - x) converts glossiness to roughness
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeInvert')
|
|
||||||
node.label = 'Invert (Gloss to Rough)'
|
|
||||||
node.location = x - 140, y - 75
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(roughness_socket, node.outputs[0])
|
|
||||||
# Inputs
|
|
||||||
node.inputs['Fac'].default_value = 1
|
|
||||||
glossiness_socket = node.inputs['Color']
|
|
||||||
|
|
||||||
x -= 250
|
|
||||||
|
|
||||||
# Mix in spec/gloss factor
|
|
||||||
if spec_factor != [1, 1, 1] or gloss_factor != 1:
|
|
||||||
if spec_factor != [1, 1, 1]:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMix')
|
|
||||||
node.data_type = 'RGBA'
|
|
||||||
node.label = 'Specular Factor'
|
|
||||||
node.location = x - 140, y
|
|
||||||
node.blend_type = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(specular_socket, node.outputs[2])
|
|
||||||
# Inputs
|
|
||||||
node.inputs['Factor'].default_value = 1.0
|
|
||||||
specular_socket = node.inputs[6]
|
|
||||||
node.inputs[7].default_value = spec_factor + [1]
|
|
||||||
|
|
||||||
if gloss_factor != 1:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
|
||||||
node.label = 'Glossiness Factor'
|
|
||||||
node.location = x - 140, y - 200
|
|
||||||
node.operation = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(glossiness_socket, node.outputs[0])
|
|
||||||
# Inputs
|
|
||||||
glossiness_socket = node.inputs[0]
|
|
||||||
node.inputs[1].default_value = gloss_factor
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
mh,
|
||||||
tex_info=spec_gloss_texture,
|
location=locs['specular'],
|
||||||
label='SPECULAR GLOSSINESS',
|
label='Specular Color',
|
||||||
location=(x, y),
|
socket=pbr_node.inputs['Specular Tint'],
|
||||||
color_socket=specular_socket,
|
factor=ext.get('specularFactor', [1, 1, 1]),
|
||||||
alpha_socket=glossiness_socket,
|
tex_info=ext.get('specularGlossinessTexture'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Glossiness
|
||||||
|
glossiness(
|
||||||
|
mh,
|
||||||
|
ext,
|
||||||
|
location=locs['glossiness'],
|
||||||
|
roughness_socket=pbr_node.inputs['Roughness'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def copy_socket(mh, copy_from, copy_to):
|
def glossiness(mh, ext, location, roughness_socket):
|
||||||
"""Copy the links/default value from one socket to another."""
|
# Glossiness = glossinessFactor * specularGlossinessTexture.alpha
|
||||||
copy_to.default_value = copy_from.default_value
|
# Roughness = 1 - Glossiness
|
||||||
for link in copy_from.links:
|
|
||||||
mh.node_tree.links.new(copy_to, link.from_socket)
|
factor = ext.get('glossinessFactor', 1)
|
||||||
|
tex_info = ext.get('specularGlossinessTexture')
|
||||||
|
if tex_info is not None:
|
||||||
|
tex_info = TextureInfo.from_dict(tex_info)
|
||||||
|
|
||||||
|
# Simple case: no texture
|
||||||
|
if tex_info is None or factor == 0:
|
||||||
|
roughness_socket.default_value = 1 - factor
|
||||||
|
return
|
||||||
|
|
||||||
|
# Bake an image with the roughness. The reason we don't do
|
||||||
|
# 1-X with a node is that won't export.
|
||||||
|
roughness_img = make_roughness_image(mh, factor, tex_info)
|
||||||
|
if roughness_img is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
texture(
|
||||||
|
mh,
|
||||||
|
tex_info,
|
||||||
|
location=location,
|
||||||
|
label='ROUGHNESS',
|
||||||
|
color_socket=None,
|
||||||
|
alpha_socket=roughness_socket,
|
||||||
|
is_data=False,
|
||||||
|
forced_image=roughness_img,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_roughness_image(mh, glossiness_factor, tex_info):
|
||||||
|
"""
|
||||||
|
Bakes the roughness (1-glossiness) into an image. The
|
||||||
|
roughness is in the alpha channel.
|
||||||
|
"""
|
||||||
|
pytexture = mh.gltf.data.textures[tex_info.index]
|
||||||
|
source = get_source(mh, pytexture)
|
||||||
|
|
||||||
|
if source is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
pyimg = mh.gltf.data.images[source]
|
||||||
|
BlenderImage.create(mh.gltf, source)
|
||||||
|
|
||||||
|
# See if cached roughness texture already exists
|
||||||
|
if hasattr(pyimg, 'blender_roughness_image_name'):
|
||||||
|
return bpy.data.images[pyimg.blender_roughness_image_name]
|
||||||
|
|
||||||
|
orig_image = bpy.data.images[pyimg.blender_image_name]
|
||||||
|
# TODO: check for placeholder image and bail
|
||||||
|
|
||||||
|
# Make a copy of the specularGlossiness texture
|
||||||
|
# Avoids interfering if it's used elsewhere
|
||||||
|
image = orig_image.copy()
|
||||||
|
|
||||||
|
w, h = image.size
|
||||||
|
pixels = np.empty(w * h * 4, dtype=np.float32)
|
||||||
|
image.pixels.foreach_get(pixels)
|
||||||
|
pixels = pixels.reshape((w, h, 4))
|
||||||
|
|
||||||
|
# Glossiness = GlossinessFactor * Texture.alpha
|
||||||
|
# Roughness = 1 - Glossiness
|
||||||
|
if glossiness_factor != 1:
|
||||||
|
pixels[:, :, 3] *= glossiness_factor
|
||||||
|
pixels[:, :, 3] *= -1
|
||||||
|
pixels[:, :, 3] += 1
|
||||||
|
|
||||||
|
pixels = pixels.reshape(w * h * 4)
|
||||||
|
image.pixels.foreach_set(pixels)
|
||||||
|
|
||||||
|
image.pack()
|
||||||
|
|
||||||
|
# Cache for reuse
|
||||||
|
pyimg.blender_roughness_image_name = image.name
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def calc_locations(mh, ext):
|
||||||
|
"""Calculate locations to place each bit of the node graph at."""
|
||||||
|
# Lay the blocks out top-to-bottom, aligned on the right
|
||||||
|
x = -200
|
||||||
|
y = 0
|
||||||
|
height = 460 # height of each block
|
||||||
|
locs = {}
|
||||||
|
|
||||||
|
locs['occlusion'] = (x, y)
|
||||||
|
if mh.pymat.occlusion_texture is not None:
|
||||||
|
y -= height
|
||||||
|
|
||||||
|
locs['diffuse'] = (x, y)
|
||||||
|
if 'diffuseTexture' in ext or mh.vertex_color:
|
||||||
|
y -= height
|
||||||
|
|
||||||
|
locs['glossiness'] = (x, y)
|
||||||
|
gloss_factor = ext.get('glossinessFactor', 1)
|
||||||
|
if 'specularGlossinessTexture' in ext and gloss_factor != 0:
|
||||||
|
y -= height
|
||||||
|
|
||||||
|
locs['normal'] = (x, y)
|
||||||
|
if mh.pymat.normal_texture is not None:
|
||||||
|
y -= height
|
||||||
|
|
||||||
|
locs['specular'] = (x, y)
|
||||||
|
if 'specularGlossinessTexture' in ext:
|
||||||
|
y -= height
|
||||||
|
|
||||||
|
locs['emission'] = (x, y)
|
||||||
|
if mh.pymat.emissive_texture is not None:
|
||||||
|
y -= height
|
||||||
|
|
||||||
|
# Center things
|
||||||
|
total_height = -y
|
||||||
|
y_offset = total_height / 2 - 20
|
||||||
|
for key in locs:
|
||||||
|
x, y = locs[key]
|
||||||
|
locs[key] = (x, y + y_offset)
|
||||||
|
|
||||||
|
return locs
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2018-2022 The glTF-Blender-IO authors
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
from ...io.com.gltf2_io import TextureInfo
|
|
||||||
from .gltf2_blender_texture import texture
|
|
||||||
|
|
||||||
def sheen( mh,
|
|
||||||
location_sheenTint,
|
|
||||||
location_sheenRoughness,
|
|
||||||
sheen_socket,
|
|
||||||
sheenTint_socket,
|
|
||||||
sheenRoughness_socket
|
|
||||||
):
|
|
||||||
|
|
||||||
x_sheenTint, y_sheenTint = location_sheenTint
|
|
||||||
x_sheenRoughness, y_sheenRoughness = location_sheenRoughness
|
|
||||||
|
|
||||||
try:
|
|
||||||
ext = mh.pymat.extensions['KHR_materials_sheen']
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
sheen_socket.default_value = 1.0
|
|
||||||
sheenTintFactor = ext.get('sheenColorFactor', [0.0, 0.0, 0.0])
|
|
||||||
tex_info_color = ext.get('sheenColorTexture')
|
|
||||||
if tex_info_color is not None:
|
|
||||||
tex_info_color = TextureInfo.from_dict(tex_info_color)
|
|
||||||
|
|
||||||
sheenRoughnessFactor = ext.get('sheenRoughnessFactor', 0.0)
|
|
||||||
tex_info_roughness = ext.get('sheenRoughnessTexture')
|
|
||||||
if tex_info_roughness is not None:
|
|
||||||
tex_info_roughness = TextureInfo.from_dict(tex_info_roughness)
|
|
||||||
|
|
||||||
if tex_info_color is None:
|
|
||||||
sheenTintFactor.extend([1.0])
|
|
||||||
sheenTint_socket.default_value = sheenTintFactor
|
|
||||||
else:
|
|
||||||
# Mix sheenTint factor
|
|
||||||
sheenTintFactor = sheenTintFactor + [1.0]
|
|
||||||
if sheenTintFactor != [1.0, 1.0, 1.0, 1.0]:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMix')
|
|
||||||
node.label = 'sheenTint Factor'
|
|
||||||
node.data_type = 'RGBA'
|
|
||||||
node.location = x_sheenTint - 140, y_sheenTint
|
|
||||||
node.blend_type = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(sheenTint_socket, node.outputs[2])
|
|
||||||
# Inputs
|
|
||||||
node.inputs['Factor'].default_value = 1.0
|
|
||||||
sheenTint_socket = node.inputs[6]
|
|
||||||
node.inputs[7].default_value = sheenTintFactor
|
|
||||||
x_sheenTint -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
|
||||||
tex_info=tex_info_color,
|
|
||||||
label='SHEEN COLOR',
|
|
||||||
location=(x_sheenTint, y_sheenTint),
|
|
||||||
color_socket=sheenTint_socket
|
|
||||||
)
|
|
||||||
|
|
||||||
if tex_info_roughness is None:
|
|
||||||
sheenRoughness_socket.default_value = sheenRoughnessFactor
|
|
||||||
else:
|
|
||||||
# Mix sheenRoughness factor
|
|
||||||
if sheenRoughnessFactor != 1.0:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
|
||||||
node.label = 'shennRoughness Factor'
|
|
||||||
node.location = x_sheenRoughness - 140, y_sheenRoughness
|
|
||||||
node.operation = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(sheenRoughness_socket, node.outputs[0])
|
|
||||||
# Inputs
|
|
||||||
sheenRoughness_socket = node.inputs[0]
|
|
||||||
node.inputs[1].default_value = sheenRoughnessFactor
|
|
||||||
x_sheenRoughness -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
|
||||||
tex_info=tex_info_roughness,
|
|
||||||
label='SHEEN ROUGHNESS',
|
|
||||||
location=(x_sheenRoughness, y_sheenRoughness),
|
|
||||||
is_data=True,
|
|
||||||
color_socket=None,
|
|
||||||
alpha_socket=sheenRoughness_socket
|
|
||||||
)
|
|
||||||
return
|
|
@ -1,94 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2018-2021 The glTF-Blender-IO authors
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from ...io.com.gltf2_io import TextureInfo
|
|
||||||
from .gltf2_blender_texture import texture
|
|
||||||
from ...io.com.gltf2_io_constants import GLTF_IOR
|
|
||||||
from ..exp.material.extensions.gltf2_blender_image import TmpImageGuard, make_temp_image_copy #TODO move to com
|
|
||||||
|
|
||||||
|
|
||||||
def specular(mh, location_specular,
|
|
||||||
location_specular_tint,
|
|
||||||
specular_socket,
|
|
||||||
specular_tint_socket):
|
|
||||||
|
|
||||||
if specular_socket is None:
|
|
||||||
return
|
|
||||||
if specular_tint_socket is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
ext = mh.pymat.extensions['KHR_materials_specular']
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
# First check if we need a texture or not -> retrieve all info needed
|
|
||||||
specular_factor = ext.get('specularFactor', 1.0)
|
|
||||||
tex_specular_info = ext.get('specularTexture')
|
|
||||||
if tex_specular_info is not None:
|
|
||||||
tex_specular_info = TextureInfo.from_dict(tex_specular_info)
|
|
||||||
|
|
||||||
specular_tint_factor = ext.get('specularColorFactor', [1.0, 1.0, 1.0])[:3]
|
|
||||||
tex_specular_tint_info = ext.get('specularColorTexture')
|
|
||||||
if tex_specular_tint_info is not None:
|
|
||||||
tex_specular_tint_info = TextureInfo.from_dict(tex_specular_tint_info)
|
|
||||||
|
|
||||||
x_specular, y_specular = location_specular
|
|
||||||
x_specularcolor, y_specularcolor = location_specular_tint
|
|
||||||
|
|
||||||
if tex_specular_info is None:
|
|
||||||
specular_socket.default_value = specular_factor / 2.0
|
|
||||||
else:
|
|
||||||
# Mix specular factor
|
|
||||||
if specular_factor != 1.0:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
|
||||||
node.label = 'Specular Factor'
|
|
||||||
node.location = x_specular - 140, y_specular
|
|
||||||
node.operation = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(specular_socket, node.outputs[0])
|
|
||||||
# Inputs
|
|
||||||
specular_socket = node.inputs[0]
|
|
||||||
node.inputs[1].default_value = specular_factor / 2.0
|
|
||||||
x_specular -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
|
||||||
tex_info=tex_specular_info,
|
|
||||||
label='SPECULAR',
|
|
||||||
location=(x_specular, y_specular),
|
|
||||||
is_data=True,
|
|
||||||
color_socket=None,
|
|
||||||
alpha_socket=specular_socket
|
|
||||||
)
|
|
||||||
|
|
||||||
if tex_specular_tint_info is None:
|
|
||||||
specular_tint_factor = list(specular_tint_factor)
|
|
||||||
specular_tint_factor.extend([1.0])
|
|
||||||
specular_tint_socket.default_value = specular_tint_factor
|
|
||||||
else:
|
|
||||||
specular_tint_factor = list(specular_tint_factor) + [1.0]
|
|
||||||
if specular_tint_factor != [1.0, 1.0, 1.0, 1.0]:
|
|
||||||
# Mix specularColorFactor
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMix')
|
|
||||||
node.label = 'SpecularColor Factor'
|
|
||||||
node.data_type = 'RGBA'
|
|
||||||
node.location = x_specularcolor - 140, y_specularcolor
|
|
||||||
node.blend_type = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(specular_tint_socket, node.outputs[2])
|
|
||||||
# Inputs
|
|
||||||
node.inputs['Factor'].default_value = 1.0
|
|
||||||
specular_tint_socket = node.inputs[6]
|
|
||||||
node.inputs[7].default_value = specular_tint_factor
|
|
||||||
x_specularcolor -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
|
||||||
tex_info=tex_specular_tint_info,
|
|
||||||
label='SPECULAR COLOR',
|
|
||||||
location=(x_specularcolor, y_specularcolor),
|
|
||||||
color_socket=specular_tint_socket,
|
|
||||||
)
|
|
@ -1,68 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2018-2022 The glTF-Blender-IO authors
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
|
|
||||||
from ...io.com.gltf2_io import TextureInfo
|
|
||||||
from .gltf2_blender_texture import texture
|
|
||||||
|
|
||||||
|
|
||||||
# [Texture] => [Separate R] => [Transmission Factor] =>
|
|
||||||
def transmission(mh, location, transmission_socket):
|
|
||||||
x, y = location
|
|
||||||
try:
|
|
||||||
ext = mh.pymat.extensions['KHR_materials_transmission']
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
transmission_factor = ext.get('transmissionFactor', 0)
|
|
||||||
|
|
||||||
# Default value is 0, so no transmission
|
|
||||||
if transmission_factor == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Activate screen refraction (for Eevee)
|
|
||||||
mh.mat.use_screen_refraction = True
|
|
||||||
|
|
||||||
tex_info = ext.get('transmissionTexture')
|
|
||||||
if tex_info is not None:
|
|
||||||
tex_info = TextureInfo.from_dict(tex_info)
|
|
||||||
|
|
||||||
if transmission_socket is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if tex_info is None:
|
|
||||||
transmission_socket.default_value = transmission_factor
|
|
||||||
return
|
|
||||||
|
|
||||||
# Mix transmission factor
|
|
||||||
if transmission_factor != 1:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
|
||||||
node.label = 'Transmission Factor'
|
|
||||||
node.location = x - 140, y
|
|
||||||
node.operation = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(transmission_socket, node.outputs[0])
|
|
||||||
# Inputs
|
|
||||||
transmission_socket = node.inputs[0]
|
|
||||||
node.inputs[1].default_value = transmission_factor
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
# Separate RGB
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeSeparateColor')
|
|
||||||
node.location = x - 150, y - 75
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(transmission_socket, node.outputs['Red'])
|
|
||||||
# Inputs
|
|
||||||
transmission_socket = node.inputs[0]
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
|
||||||
tex_info=tex_info,
|
|
||||||
label='TRANSMISSION',
|
|
||||||
location=(x, y),
|
|
||||||
is_data=True,
|
|
||||||
color_socket=transmission_socket,
|
|
||||||
)
|
|
@ -2,38 +2,51 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from .gltf2_blender_pbrMetallicRoughness import base_color, make_output_nodes
|
from .gltf2_blender_pbrMetallicRoughness import base_color
|
||||||
|
|
||||||
|
|
||||||
def unlit(mh):
|
def unlit(mh):
|
||||||
"""Creates node tree for unlit materials."""
|
"""Creates node tree for unlit materials."""
|
||||||
# Emission node for the base color
|
# Emission node for the base color
|
||||||
emission_node = mh.node_tree.nodes.new('ShaderNodeEmission')
|
emission_node = mh.nodes.new('ShaderNodeEmission')
|
||||||
emission_node.location = 10, 126
|
emission_node.location = 10, 126
|
||||||
|
|
||||||
# Lightpath trick: makes Emission visible only to camera rays.
|
# Create a "Lightpath trick": makes Emission visible only to
|
||||||
|
# camera rays, so it won't "glow" in Cycles.
|
||||||
|
#
|
||||||
# [Is Camera Ray] => [Mix] =>
|
# [Is Camera Ray] => [Mix] =>
|
||||||
# [Transparent] => [ ]
|
# [Transparent] => [ ]
|
||||||
# [Emission] => [ ]
|
# [Emission] => [ ]
|
||||||
lightpath_node = mh.node_tree.nodes.new('ShaderNodeLightPath')
|
lightpath_node = mh.nodes.new('ShaderNodeLightPath')
|
||||||
transparent_node = mh.node_tree.nodes.new('ShaderNodeBsdfTransparent')
|
transparent_node = mh.nodes.new('ShaderNodeBsdfTransparent')
|
||||||
mix_node = mh.node_tree.nodes.new('ShaderNodeMixShader')
|
mix_node = mh.nodes.new('ShaderNodeMixShader')
|
||||||
lightpath_node.location = 10, 600
|
lightpath_node.location = 10, 600
|
||||||
transparent_node.location = 10, 240
|
transparent_node.location = 10, 240
|
||||||
mix_node.location = 260, 320
|
mix_node.location = 260, 320
|
||||||
mh.node_tree.links.new(mix_node.inputs['Fac'], lightpath_node.outputs['Is Camera Ray'])
|
mh.links.new(mix_node.inputs['Fac'], lightpath_node.outputs['Is Camera Ray'])
|
||||||
mh.node_tree.links.new(mix_node.inputs[1], transparent_node.outputs[0])
|
mh.links.new(mix_node.inputs[1], transparent_node.outputs[0])
|
||||||
mh.node_tree.links.new(mix_node.inputs[2], emission_node.outputs[0])
|
mh.links.new(mix_node.inputs[2], emission_node.outputs[0])
|
||||||
|
|
||||||
_emission_socket, alpha_socket, _ = make_output_nodes(
|
# Material output
|
||||||
mh,
|
alpha_socket = None
|
||||||
location=(420, 280) if mh.is_opaque() else (150, 130),
|
out_node = mh.nodes.new('ShaderNodeOutputMaterial')
|
||||||
additional_location=None, #No additional location needed for Unlit
|
if mh.is_opaque():
|
||||||
shader_socket=mix_node.outputs[0],
|
out_node.location = 490, 290
|
||||||
make_emission_socket=False,
|
mh.links.new(out_node.inputs[0], mix_node.outputs[0])
|
||||||
make_alpha_socket=not mh.is_opaque(),
|
else:
|
||||||
make_volume_socket=None # Not possible to have KHR_materials_volume with unlit
|
# Create a "Mix with Transparent" setup so there's a
|
||||||
)
|
# place to put Alpha.
|
||||||
|
#
|
||||||
|
# Alpha => [Mix] => [Output]
|
||||||
|
# [Transparent] => [ ]
|
||||||
|
# Color => [ ]
|
||||||
|
mix2_node = mh.nodes.new('ShaderNodeMixShader')
|
||||||
|
alpha_socket = mix2_node.inputs['Fac']
|
||||||
|
mix2_node.location = 490, -50
|
||||||
|
out_node.location = 700, -70
|
||||||
|
mh.links.new(mix2_node.inputs[1], transparent_node.outputs[0])
|
||||||
|
mh.links.new(mix2_node.inputs[2], mix_node.outputs[0])
|
||||||
|
mh.links.new(out_node.inputs[0], mix2_node.outputs[0])
|
||||||
|
|
||||||
base_color(
|
base_color(
|
||||||
mh,
|
mh,
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2018-2021 The glTF-Blender-IO authors
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
from ...io.com.gltf2_io import TextureInfo
|
|
||||||
from .gltf2_blender_texture import texture
|
|
||||||
|
|
||||||
def volume(mh, location, volume_socket, thickness_socket):
|
|
||||||
# implementation based on https://github.com/KhronosGroup/glTF-Blender-IO/issues/1454#issuecomment-928319444
|
|
||||||
try:
|
|
||||||
ext = mh.pymat.extensions['KHR_materials_volume']
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Attenuation Color
|
|
||||||
attenuationColor = \
|
|
||||||
mh.pymat.extensions['KHR_materials_volume'] \
|
|
||||||
.get('attenuationColor')
|
|
||||||
# glTF is color3, Blender adds alpha
|
|
||||||
if attenuationColor is None:
|
|
||||||
attenuationColor = [1.0, 1.0, 1.0, 1.0]
|
|
||||||
else:
|
|
||||||
attenuationColor.extend([1.0])
|
|
||||||
volume_socket.node.inputs[0].default_value = attenuationColor
|
|
||||||
|
|
||||||
# Attenuation Distance / Density
|
|
||||||
attenuationDistance = mh.pymat.extensions['KHR_materials_volume'].get('attenuationDistance')
|
|
||||||
if attenuationDistance is None:
|
|
||||||
density = 0
|
|
||||||
else:
|
|
||||||
density = 1.0 / attenuationDistance
|
|
||||||
volume_socket.node.inputs[1].default_value = density
|
|
||||||
|
|
||||||
# thicknessFactor / thicknessTexture
|
|
||||||
x, y = location
|
|
||||||
try:
|
|
||||||
ext = mh.pymat.extensions['KHR_materials_volume']
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
thickness_factor = ext.get('thicknessFactor', 0)
|
|
||||||
tex_info = ext.get('thicknessTexture')
|
|
||||||
if tex_info is not None:
|
|
||||||
tex_info = TextureInfo.from_dict(tex_info)
|
|
||||||
|
|
||||||
if thickness_socket is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if tex_info is None:
|
|
||||||
thickness_socket.default_value = thickness_factor
|
|
||||||
return
|
|
||||||
|
|
||||||
# Mix thickness factor
|
|
||||||
if thickness_factor != 1:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
|
||||||
node.label = 'Thickness Factor'
|
|
||||||
node.location = x - 140, y
|
|
||||||
node.operation = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(thickness_socket, node.outputs[0])
|
|
||||||
# Inputs
|
|
||||||
thickness_socket = node.inputs[0]
|
|
||||||
node.inputs[1].default_value = thickness_factor
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
# Separate RGB
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeSeparateColor')
|
|
||||||
node.location = x - 150, y - 75
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(thickness_socket, node.outputs['Green'])
|
|
||||||
# Inputs
|
|
||||||
thickness_socket = node.inputs[0]
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
|
||||||
tex_info=tex_info,
|
|
||||||
label='THICKNESS',
|
|
||||||
location=(x, y),
|
|
||||||
is_data=True,
|
|
||||||
color_socket=thickness_socket,
|
|
||||||
)
|
|
@ -129,7 +129,7 @@ class BlenderNodeAnim():
|
|||||||
if values[i].dot(values[i-1]) < 0:
|
if values[i].dot(values[i-1]) < 0:
|
||||||
values[i] = -values[i]
|
values[i] = -values[i]
|
||||||
|
|
||||||
fps = bpy.context.scene.render.fps
|
fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
|
|
||||||
coords = [0] * (2 * len(keys))
|
coords = [0] * (2 * len(keys))
|
||||||
coords[::2] = (key[0] * fps for key in keys)
|
coords[::2] = (key[0] * fps for key in keys)
|
||||||
|
@ -28,7 +28,7 @@ class BlenderWeightAnim():
|
|||||||
|
|
||||||
node = gltf.data.nodes[node_idx]
|
node = gltf.data.nodes[node_idx]
|
||||||
obj = vnode.blender_object
|
obj = vnode.blender_object
|
||||||
fps = bpy.context.scene.render.fps
|
fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
|
|
||||||
animation = gltf.data.animations[anim_idx]
|
animation = gltf.data.animations[anim_idx]
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ class BlenderImage():
|
|||||||
blender_image = create_from_data(gltf, img_idx)
|
blender_image = create_from_data(gltf, img_idx)
|
||||||
|
|
||||||
if blender_image:
|
if blender_image:
|
||||||
|
blender_image.alpha_mode = 'CHANNEL_PACKED'
|
||||||
img.blender_image_name = blender_image.name
|
img.blender_image_name = blender_image.name
|
||||||
|
|
||||||
import_user_extensions('gather_import_image_after_hook', gltf, img, blender_image)
|
import_user_extensions('gather_import_image_after_hook', gltf, img, blender_image)
|
||||||
|
191
io_scene_gltf2/blender/imp/gltf2_blender_material_utils.py
Normal file
191
io_scene_gltf2/blender/imp/gltf2_blender_material_utils.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2018-2021 The glTF-Blender-IO authors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from ...io.com.gltf2_io import (
|
||||||
|
TextureInfo,
|
||||||
|
MaterialNormalTextureInfoClass,
|
||||||
|
MaterialPBRMetallicRoughness,
|
||||||
|
)
|
||||||
|
from .gltf2_blender_texture import texture
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialHelper:
|
||||||
|
"""Helper class. Stores material stuff to be passed around everywhere."""
|
||||||
|
def __init__(self, gltf, pymat, mat, vertex_color):
|
||||||
|
self.gltf = gltf
|
||||||
|
self.pymat = pymat
|
||||||
|
self.mat = mat
|
||||||
|
self.node_tree = mat.node_tree
|
||||||
|
self.nodes = mat.node_tree.nodes
|
||||||
|
self.links = mat.node_tree.links
|
||||||
|
self.vertex_color = vertex_color
|
||||||
|
if pymat.pbr_metallic_roughness is None:
|
||||||
|
pymat.pbr_metallic_roughness = \
|
||||||
|
MaterialPBRMetallicRoughness.from_dict({})
|
||||||
|
self.settings_node = None
|
||||||
|
|
||||||
|
def is_opaque(self):
|
||||||
|
alpha_mode = self.pymat.alpha_mode
|
||||||
|
return alpha_mode is None or alpha_mode == 'OPAQUE'
|
||||||
|
|
||||||
|
def needs_emissive(self):
|
||||||
|
return (
|
||||||
|
self.pymat.emissive_texture is not None or
|
||||||
|
(self.pymat.emissive_factor or [0, 0, 0]) != [0, 0, 0]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_ext(self, ext_name, default=None):
|
||||||
|
if not self.pymat.extensions:
|
||||||
|
return default
|
||||||
|
return self.pymat.extensions.get(ext_name, default)
|
||||||
|
|
||||||
|
|
||||||
|
# Creates nodes for multiplying a texture channel and scalar factor.
|
||||||
|
# [Texture] => [Sep RGB] => [Mul Factor] => socket
|
||||||
|
def scalar_factor_and_texture(
|
||||||
|
mh: MaterialHelper,
|
||||||
|
location,
|
||||||
|
label,
|
||||||
|
socket, # socket to connect to
|
||||||
|
factor, # scalar factor
|
||||||
|
tex_info, # texture
|
||||||
|
channel, # texture channel to use (0-4)
|
||||||
|
):
|
||||||
|
if isinstance(tex_info, dict):
|
||||||
|
tex_info = TextureInfo.from_dict(tex_info)
|
||||||
|
|
||||||
|
x, y = location
|
||||||
|
|
||||||
|
if socket is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if tex_info is None:
|
||||||
|
socket.default_value = factor
|
||||||
|
return
|
||||||
|
|
||||||
|
if factor != 1.0:
|
||||||
|
node = mh.nodes.new('ShaderNodeMath')
|
||||||
|
node.label = f'{label} Factor'
|
||||||
|
node.location = x - 140, y
|
||||||
|
node.operation = 'MULTIPLY'
|
||||||
|
# Outputs
|
||||||
|
mh.links.new(socket, node.outputs[0])
|
||||||
|
# Inputs
|
||||||
|
socket = node.inputs[0]
|
||||||
|
node.inputs[1].default_value = factor
|
||||||
|
|
||||||
|
x -= 200
|
||||||
|
|
||||||
|
if channel != 4:
|
||||||
|
# Separate RGB
|
||||||
|
node = mh.nodes.new('ShaderNodeSeparateColor')
|
||||||
|
node.location = x - 150, y - 75
|
||||||
|
# Outputs
|
||||||
|
mh.links.new(socket, node.outputs[channel])
|
||||||
|
# Inputs
|
||||||
|
socket = node.inputs[0]
|
||||||
|
|
||||||
|
x -= 200
|
||||||
|
|
||||||
|
texture(
|
||||||
|
mh,
|
||||||
|
tex_info=tex_info,
|
||||||
|
label=label.upper(),
|
||||||
|
location=(x, y),
|
||||||
|
is_data=channel < 4,
|
||||||
|
color_socket=socket if channel != 4 else None,
|
||||||
|
alpha_socket=socket if channel == 4 else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Creates nodes for multiplying a texture color and color factor.
|
||||||
|
# [Texture] => [Mix Factor] => socket
|
||||||
|
def color_factor_and_texture(
|
||||||
|
mh: MaterialHelper,
|
||||||
|
location,
|
||||||
|
label,
|
||||||
|
socket, # socket to connect to
|
||||||
|
factor, # color factor
|
||||||
|
tex_info, # texture
|
||||||
|
):
|
||||||
|
if isinstance(tex_info, dict):
|
||||||
|
tex_info = TextureInfo.from_dict(tex_info)
|
||||||
|
|
||||||
|
x, y = location
|
||||||
|
|
||||||
|
if socket is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if tex_info is None:
|
||||||
|
socket.default_value = [*factor, 1]
|
||||||
|
return
|
||||||
|
|
||||||
|
if factor != [1, 1, 1]:
|
||||||
|
node = mh.nodes.new('ShaderNodeMix')
|
||||||
|
node.data_type = 'RGBA'
|
||||||
|
node.label = f'{label} Factor'
|
||||||
|
node.location = x - 140, y
|
||||||
|
node.blend_type = 'MULTIPLY'
|
||||||
|
# Outputs
|
||||||
|
mh.links.new(socket, node.outputs[2])
|
||||||
|
# Inputs
|
||||||
|
node.inputs['Factor'].default_value = 1
|
||||||
|
socket = node.inputs[6]
|
||||||
|
node.inputs[7].default_value = [*factor, 1]
|
||||||
|
|
||||||
|
x -= 200
|
||||||
|
|
||||||
|
texture(
|
||||||
|
mh,
|
||||||
|
tex_info=tex_info,
|
||||||
|
label=label.upper(),
|
||||||
|
location=(x, y),
|
||||||
|
is_data=False,
|
||||||
|
color_socket=socket,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# [Texture] => [Normal Map] => socket
|
||||||
|
def normal_map(
|
||||||
|
mh: MaterialHelper,
|
||||||
|
location,
|
||||||
|
label,
|
||||||
|
socket,
|
||||||
|
tex_info,
|
||||||
|
):
|
||||||
|
if isinstance(tex_info, dict):
|
||||||
|
tex_info = MaterialNormalTextureInfoClass.from_dict(tex_info)
|
||||||
|
|
||||||
|
if not tex_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
x,y = location
|
||||||
|
|
||||||
|
# Normal map
|
||||||
|
node = mh.nodes.new('ShaderNodeNormalMap')
|
||||||
|
node.location = x - 150, y - 40
|
||||||
|
# Set UVMap
|
||||||
|
uv_idx = tex_info.tex_coord or 0
|
||||||
|
try:
|
||||||
|
uv_idx = tex_info.extensions['KHR_texture_transform']['texCoord']
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
node.uv_map = 'UVMap' if uv_idx == 0 else 'UVMap.%03d' % uv_idx
|
||||||
|
# Set strength
|
||||||
|
scale = tex_info.scale
|
||||||
|
scale = scale if scale is not None else 1
|
||||||
|
node.inputs['Strength'].default_value = scale
|
||||||
|
# Outputs
|
||||||
|
mh.links.new(socket, node.outputs['Normal'])
|
||||||
|
|
||||||
|
x -= 200
|
||||||
|
|
||||||
|
texture(
|
||||||
|
mh,
|
||||||
|
tex_info=tex_info,
|
||||||
|
label=label.upper(),
|
||||||
|
location=(x, y),
|
||||||
|
is_data=True,
|
||||||
|
color_socket=node.inputs['Color'],
|
||||||
|
)
|
@ -11,6 +11,7 @@ from ...io.imp.gltf2_io_binary import BinaryData
|
|||||||
from ...io.com.gltf2_io_constants import DataType, ComponentType
|
from ...io.com.gltf2_io_constants import DataType, ComponentType
|
||||||
from ...blender.com.gltf2_blender_conversion import get_attribute_type
|
from ...blender.com.gltf2_blender_conversion import get_attribute_type
|
||||||
from ..com.gltf2_blender_extras import set_extras
|
from ..com.gltf2_blender_extras import set_extras
|
||||||
|
from ..com.gltf2_blender_utils import fast_structured_np_unique
|
||||||
from .gltf2_blender_material import BlenderMaterial
|
from .gltf2_blender_material import BlenderMaterial
|
||||||
from .gltf2_io_draco_compression_extension import decode_primitive
|
from .gltf2_io_draco_compression_extension import decode_primitive
|
||||||
|
|
||||||
@ -296,21 +297,22 @@ def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob):
|
|||||||
# Start creating things
|
# Start creating things
|
||||||
|
|
||||||
mesh.vertices.add(len(vert_locs))
|
mesh.vertices.add(len(vert_locs))
|
||||||
mesh.vertices.foreach_set('co', squish(vert_locs))
|
position_attribute = attribute_ensure(mesh.attributes, 'position', 'FLOAT_VECTOR', 'POINT')
|
||||||
|
position_attribute.data.foreach_set('vector', squish(vert_locs, np.float32))
|
||||||
|
|
||||||
mesh.loops.add(len(loop_vidxs))
|
mesh.loops.add(len(loop_vidxs))
|
||||||
mesh.loops.foreach_set('vertex_index', loop_vidxs)
|
corner_vert_attribute = attribute_ensure(mesh.attributes, '.corner_vert', 'INT', 'CORNER')
|
||||||
|
corner_vert_attribute.data.foreach_set('value', squish(loop_vidxs, np.intc))
|
||||||
|
|
||||||
mesh.edges.add(len(edge_vidxs) // 2)
|
mesh.edges.add(len(edge_vidxs) // 2)
|
||||||
mesh.edges.foreach_set('vertices', edge_vidxs)
|
edge_verts_attribute = attribute_ensure(mesh.attributes, '.edge_verts', 'INT32_2D', 'EDGE')
|
||||||
|
edge_verts_attribute.data.foreach_set('value', squish(edge_vidxs, np.intc))
|
||||||
|
|
||||||
mesh.polygons.add(num_faces)
|
mesh.polygons.add(num_faces)
|
||||||
|
|
||||||
# All polys are tris
|
# All polys are tris
|
||||||
loop_starts = np.arange(0, 3 * num_faces, step=3)
|
loop_starts = np.arange(0, 3 * num_faces, step=3)
|
||||||
loop_totals = np.full(num_faces, 3)
|
|
||||||
mesh.polygons.foreach_set('loop_start', loop_starts)
|
mesh.polygons.foreach_set('loop_start', loop_starts)
|
||||||
mesh.polygons.foreach_set('loop_total', loop_totals)
|
|
||||||
|
|
||||||
for uv_i in range(num_uvs):
|
for uv_i in range(num_uvs):
|
||||||
name = 'UVMap' if uv_i == 0 else 'UVMap.%03d' % uv_i
|
name = 'UVMap' if uv_i == 0 else 'UVMap.%03d' % uv_i
|
||||||
@ -320,18 +322,13 @@ def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob):
|
|||||||
print("WARNING: UV map is ignored because the maximum number of UV layers has been reached.")
|
print("WARNING: UV map is ignored because the maximum number of UV layers has been reached.")
|
||||||
break
|
break
|
||||||
|
|
||||||
layer.data.foreach_set('uv', squish(loop_uvs[uv_i]))
|
layer.uv.foreach_set('vector', squish(loop_uvs[uv_i], np.float32))
|
||||||
|
|
||||||
for col_i in range(num_cols):
|
for col_i in range(num_cols):
|
||||||
name = 'Col' if col_i == 0 else 'Col.%03d' % col_i
|
name = 'Color' if col_i == 0 else 'Color.%03d' % col_i
|
||||||
layer = mesh.vertex_colors.new(name=name)
|
layer = mesh.color_attributes.new(name, 'BYTE_COLOR', 'CORNER')
|
||||||
|
|
||||||
if layer is None:
|
layer.data.foreach_set('color', squish(loop_cols[col_i], np.float32))
|
||||||
print("WARNING: Vertex colors are ignored because the maximum number of vertex color layers has been "
|
|
||||||
"reached.")
|
|
||||||
break
|
|
||||||
|
|
||||||
mesh.color_attributes[layer.name].data.foreach_set('color', squish(loop_cols[col_i]))
|
|
||||||
|
|
||||||
# Make sure the first Vertex Color Attribute is the rendered one
|
# Make sure the first Vertex Color Attribute is the rendered one
|
||||||
if num_cols > 0:
|
if num_cols > 0:
|
||||||
@ -370,7 +367,7 @@ def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob):
|
|||||||
|
|
||||||
ob.shape_key_add(name=sk_name)
|
ob.shape_key_add(name=sk_name)
|
||||||
key_block = mesh.shape_keys.key_blocks[sk_name]
|
key_block = mesh.shape_keys.key_blocks[sk_name]
|
||||||
key_block.data.foreach_set('co', squish(sk_vert_locs[sk_i]))
|
key_block.points.foreach_set('co', squish(sk_vert_locs[sk_i], np.float32))
|
||||||
|
|
||||||
sk_i += 1
|
sk_i += 1
|
||||||
|
|
||||||
@ -385,7 +382,8 @@ def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob):
|
|||||||
and 'mappings' in prim.extensions['KHR_materials_variants'].keys()
|
and 'mappings' in prim.extensions['KHR_materials_variants'].keys()
|
||||||
|
|
||||||
if has_materials:
|
if has_materials:
|
||||||
material_indices = np.empty(num_faces, dtype=np.uint32)
|
bl_material_index_dtype = np.intc
|
||||||
|
material_indices = np.empty(num_faces, dtype=bl_material_index_dtype)
|
||||||
empty_material_slot_index = None
|
empty_material_slot_index = None
|
||||||
f = 0
|
f = 0
|
||||||
|
|
||||||
@ -448,7 +446,8 @@ def do_primitives(gltf, mesh_idx, skin_idx, mesh, ob):
|
|||||||
vari = variant_primitive.variants.add()
|
vari = variant_primitive.variants.add()
|
||||||
vari.variant.variant_idx = variant
|
vari.variant.variant_idx = variant
|
||||||
|
|
||||||
mesh.polygons.foreach_set('material_index', material_indices)
|
material_index_attribute = attribute_ensure(mesh.attributes, 'material_index', 'INT', 'FACE')
|
||||||
|
material_index_attribute.data.foreach_set('value', material_indices)
|
||||||
|
|
||||||
# Custom Attributes
|
# Custom Attributes
|
||||||
for idx, attr in enumerate(attributes):
|
for idx, attr in enumerate(attributes):
|
||||||
@ -534,27 +533,54 @@ def points_edges_tris(mode, indices):
|
|||||||
# 0---2---4
|
# 0---2---4
|
||||||
# \ / \ /
|
# \ / \ /
|
||||||
# 1---3
|
# 1---3
|
||||||
# TODO: numpyify
|
# in: 01234
|
||||||
def alternate(i, xs):
|
# out: 012132234
|
||||||
even = i % 2 == 0
|
# out (viewed as triplets): 012, 132, 234
|
||||||
return xs if even else (xs[0], xs[2], xs[1])
|
tris = np.empty((len(indices) - 2) * 3, dtype=np.uint32)
|
||||||
tris = np.array([
|
# 012__
|
||||||
alternate(i, (indices[i], indices[i + 1], indices[i + 2]))
|
first_indices = indices[:-2]
|
||||||
for i in range(0, len(indices) - 2)
|
# _123_
|
||||||
])
|
second_indices = indices[1:-1]
|
||||||
tris = squish(tris)
|
# __234
|
||||||
|
third_indices = indices[2:]
|
||||||
|
|
||||||
|
# Each triplet starts with the first index
|
||||||
|
# 0__, 1__, 2__ <- 012__
|
||||||
|
tris[0::3] = first_indices
|
||||||
|
|
||||||
|
# Even triplets end with the next two indices in order
|
||||||
|
# _1_, ___, _3_ <- _1_3_ <- _123_
|
||||||
|
# 01_, 1__, 23_
|
||||||
|
tris[1::6] = second_indices[0::2]
|
||||||
|
# __2, ___, __4 <- __2_4 <- __234
|
||||||
|
# 012, 1__, 234
|
||||||
|
tris[2::6] = third_indices[0::2]
|
||||||
|
|
||||||
|
# Odd triplets end with the next two indices in reverse order
|
||||||
|
# ___, _3_, ___ <- ___3_ <- __234
|
||||||
|
# 012, 13_, 234
|
||||||
|
tris[4::6] = third_indices[1::2]
|
||||||
|
# ___, __2, ___ <- __2__ <- _123_
|
||||||
|
# 012, 132, 234
|
||||||
|
tris[5::6] = second_indices[1::2]
|
||||||
|
|
||||||
elif mode == 6:
|
elif mode == 6:
|
||||||
# TRIANGLE FAN
|
# TRIANGLE FAN
|
||||||
# 3---2
|
# 3---2
|
||||||
# / \ / \
|
# / \ / \
|
||||||
# 4---0---1
|
# 4---0---1
|
||||||
# TODO: numpyify
|
# in: 01234
|
||||||
tris = np.array([
|
# out: 012023034
|
||||||
(indices[0], indices[i], indices[i + 1])
|
# out (viewed as triplets): 012, 023, 034
|
||||||
for i in range(1, len(indices) - 1)
|
# Start filled with the first index
|
||||||
])
|
# 000, 000, 000
|
||||||
tris = squish(tris)
|
tris = np.full((len(indices) - 2) * 3, indices[0], dtype=np.uint32)
|
||||||
|
# _1_, _2_, _3_ <- _123_
|
||||||
|
# 010, 020, 030
|
||||||
|
tris[1::3] = indices[1:-1]
|
||||||
|
# __2, __3, __4 <- __234
|
||||||
|
# 012, 023, 034
|
||||||
|
tris[2::3] = indices[2:]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Exception('primitive mode unimplemented: %d' % mode)
|
raise Exception('primitive mode unimplemented: %d' % mode)
|
||||||
@ -562,9 +588,10 @@ def points_edges_tris(mode, indices):
|
|||||||
return points, edges, tris
|
return points, edges, tris
|
||||||
|
|
||||||
|
|
||||||
def squish(array):
|
def squish(array, dtype=None):
|
||||||
"""Squish nD array into 1D array (required by foreach_set)."""
|
"""Squish nD array into a C-contiguous (required for faster access with the buffer protocol in foreach_set) 1D array
|
||||||
return array.reshape(array.size)
|
(required by foreach_set). Optionally converting the array to a different dtype."""
|
||||||
|
return np.ascontiguousarray(array, dtype=dtype).reshape(array.size)
|
||||||
|
|
||||||
|
|
||||||
def colors_rgb_to_rgba(rgb):
|
def colors_rgb_to_rgba(rgb):
|
||||||
@ -646,6 +673,15 @@ def normalize_vecs(vectors):
|
|||||||
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
|
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
|
||||||
np.divide(vectors, norms, out=vectors, where=norms != 0)
|
np.divide(vectors, norms, out=vectors, where=norms != 0)
|
||||||
|
|
||||||
|
def attribute_ensure(attributes, name, data_type, domain):
|
||||||
|
attribute = attributes.get(name)
|
||||||
|
if attribute is None:
|
||||||
|
return attributes.new(name, data_type, domain)
|
||||||
|
if attribute.domain == domain and attribute.data_type == data_type:
|
||||||
|
return attribute
|
||||||
|
# There is an existing attribute, but it has the wrong domain or data_type.
|
||||||
|
attributes.remove(attribute)
|
||||||
|
return attributes.new(name, data_type, domain)
|
||||||
|
|
||||||
def set_poly_smoothing(gltf, pymesh, mesh, vert_normals, loop_vidxs):
|
def set_poly_smoothing(gltf, pymesh, mesh, vert_normals, loop_vidxs):
|
||||||
num_polys = len(mesh.polygons)
|
num_polys = len(mesh.polygons)
|
||||||
@ -656,14 +692,15 @@ def set_poly_smoothing(gltf, pymesh, mesh, vert_normals, loop_vidxs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if gltf.import_settings['import_shading'] == "SMOOTH":
|
if gltf.import_settings['import_shading'] == "SMOOTH":
|
||||||
poly_smooths = np.full(num_polys, True)
|
poly_sharps = np.full(num_polys, False)
|
||||||
f = 0
|
f = 0
|
||||||
for prim in pymesh.primitives:
|
for prim in pymesh.primitives:
|
||||||
if 'NORMAL' not in prim.attributes:
|
if 'NORMAL' not in prim.attributes:
|
||||||
# Primitives with no NORMALs should use flat shading
|
# Primitives with no NORMALs should use flat shading
|
||||||
poly_smooths[f:f + prim.num_faces].fill(False)
|
poly_sharps[f:f + prim.num_faces].fill(True)
|
||||||
f += prim.num_faces
|
f += prim.num_faces
|
||||||
mesh.polygons.foreach_set('use_smooth', poly_smooths)
|
sharp_face_attribute = attribute_ensure(mesh.attributes, 'sharp_face', 'BOOLEAN', 'FACE')
|
||||||
|
sharp_face_attribute.data.foreach_set('value', poly_sharps)
|
||||||
return
|
return
|
||||||
|
|
||||||
assert gltf.import_settings['import_shading'] == "NORMALS"
|
assert gltf.import_settings['import_shading'] == "NORMALS"
|
||||||
@ -671,17 +708,17 @@ def set_poly_smoothing(gltf, pymesh, mesh, vert_normals, loop_vidxs):
|
|||||||
# Try to guess which polys should be flat based on the fact that all the
|
# Try to guess which polys should be flat based on the fact that all the
|
||||||
# loop normals for a flat poly are = the poly's normal.
|
# loop normals for a flat poly are = the poly's normal.
|
||||||
|
|
||||||
poly_smooths = np.empty(num_polys, dtype=bool)
|
poly_sharps = np.empty(num_polys, dtype=bool)
|
||||||
|
|
||||||
poly_normals = np.empty(num_polys * 3, dtype=np.float32)
|
poly_normals = np.empty(num_polys * 3, dtype=np.float32)
|
||||||
mesh.polygons.foreach_get('normal', poly_normals)
|
mesh.polygon_normals.foreach_get('vector', poly_normals)
|
||||||
poly_normals = poly_normals.reshape(num_polys, 3)
|
poly_normals = poly_normals.reshape(num_polys, 3)
|
||||||
|
|
||||||
f = 0
|
f = 0
|
||||||
for prim in pymesh.primitives:
|
for prim in pymesh.primitives:
|
||||||
if 'NORMAL' not in prim.attributes:
|
if 'NORMAL' not in prim.attributes:
|
||||||
# Primitives with no NORMALs should use flat shading
|
# Primitives with no NORMALs should use flat shading
|
||||||
poly_smooths[f:f + prim.num_faces].fill(False)
|
poly_sharps[f:f + prim.num_faces].fill(True)
|
||||||
f += prim.num_faces
|
f += prim.num_faces
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -704,11 +741,12 @@ def set_poly_smoothing(gltf, pymesh, mesh, vert_normals, loop_vidxs):
|
|||||||
dot_prods = np.sum(vert_ns * poly_ns, axis=1)
|
dot_prods = np.sum(vert_ns * poly_ns, axis=1)
|
||||||
np.logical_or(smooth, dot_prods <= 0.9999999, out=smooth)
|
np.logical_or(smooth, dot_prods <= 0.9999999, out=smooth)
|
||||||
|
|
||||||
poly_smooths[f:f + prim.num_faces] = smooth
|
np.logical_not(smooth, out=poly_sharps[f:f + prim.num_faces])
|
||||||
|
|
||||||
f += prim.num_faces
|
f += prim.num_faces
|
||||||
|
|
||||||
mesh.polygons.foreach_set('use_smooth', poly_smooths)
|
sharp_face_attribute = attribute_ensure(mesh.attributes, 'sharp_face', 'BOOLEAN', 'FACE')
|
||||||
|
sharp_face_attribute.data.foreach_set('value', poly_sharps)
|
||||||
|
|
||||||
|
|
||||||
def merge_duplicate_verts(vert_locs, vert_normals, vert_joints, vert_weights, sk_vert_locs, loop_vidxs, edge_vidxs, attribute_data):
|
def merge_duplicate_verts(vert_locs, vert_normals, vert_joints, vert_weights, sk_vert_locs, loop_vidxs, edge_vidxs, attribute_data):
|
||||||
@ -766,7 +804,7 @@ def merge_duplicate_verts(vert_locs, vert_normals, vert_joints, vert_weights, sk
|
|||||||
dots['sk%dy' % i] = locs[:, 1]
|
dots['sk%dy' % i] = locs[:, 1]
|
||||||
dots['sk%dz' % i] = locs[:, 2]
|
dots['sk%dz' % i] = locs[:, 2]
|
||||||
|
|
||||||
unique_dots, unique_ind, inv_indices = np.unique(dots, return_index=True, return_inverse=True)
|
unique_dots, unique_ind, inv_indices = fast_structured_np_unique(dots, return_index=True, return_inverse=True)
|
||||||
|
|
||||||
loop_vidxs = inv_indices[loop_vidxs]
|
loop_vidxs = inv_indices[loop_vidxs]
|
||||||
edge_vidxs = inv_indices[edge_vidxs]
|
edge_vidxs = inv_indices[edge_vidxs]
|
||||||
|
@ -229,8 +229,9 @@ class BlenderNode():
|
|||||||
|
|
||||||
if gltf.import_settings['bone_heuristic'] == "BLENDER":
|
if gltf.import_settings['bone_heuristic'] == "BLENDER":
|
||||||
pose_bone.custom_shape = bpy.data.objects[gltf.bone_shape]
|
pose_bone.custom_shape = bpy.data.objects[gltf.bone_shape]
|
||||||
armature_max_dim = max([blender_arma.dimensions[0] / blender_arma.scale[0], blender_arma.dimensions[1] / blender_arma.scale[1], blender_arma.dimensions[2] / blender_arma.scale[2]])
|
armature_min_dim = min([blender_arma.dimensions[0] / blender_arma.scale[0], blender_arma.dimensions[1] / blender_arma.scale[1], blender_arma.dimensions[2] / blender_arma.scale[2]])
|
||||||
pose_bone.custom_shape_scale_xyz = Vector([armature_max_dim * 0.2] * 3)
|
pose_bone.custom_shape_scale_xyz = Vector([armature_min_dim * 0.05] * 3)
|
||||||
|
pose_bone.use_custom_shape_bone_size = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_mesh_object(gltf, vnode):
|
def create_mesh_object(gltf, vnode):
|
||||||
|
@ -8,80 +8,39 @@ from ...io.com.gltf2_io_constants import GLTF_IOR
|
|||||||
from ...io.com.gltf2_io import TextureInfo, MaterialPBRMetallicRoughness
|
from ...io.com.gltf2_io import TextureInfo, MaterialPBRMetallicRoughness
|
||||||
from ..com.gltf2_blender_material_helpers import get_gltf_node_name, create_settings_group
|
from ..com.gltf2_blender_material_helpers import get_gltf_node_name, create_settings_group
|
||||||
from .gltf2_blender_texture import texture
|
from .gltf2_blender_texture import texture
|
||||||
from .gltf2_blender_KHR_materials_clearcoat import \
|
|
||||||
clearcoat, clearcoat_roughness, clearcoat_normal
|
|
||||||
from .gltf2_blender_KHR_materials_transmission import transmission
|
|
||||||
from .gltf2_blender_KHR_materials_ior import ior
|
|
||||||
from .gltf2_blender_KHR_materials_volume import volume
|
|
||||||
from .gltf2_blender_KHR_materials_specular import specular
|
|
||||||
from .gltf2_blender_KHR_materials_sheen import sheen
|
|
||||||
from .gltf2_blender_KHR_materials_anisotropy import anisotropy
|
from .gltf2_blender_KHR_materials_anisotropy import anisotropy
|
||||||
|
from .gltf2_blender_material_utils import \
|
||||||
class MaterialHelper:
|
MaterialHelper, scalar_factor_and_texture, color_factor_and_texture, normal_map
|
||||||
"""Helper class. Stores material stuff to be passed around everywhere."""
|
|
||||||
def __init__(self, gltf, pymat, mat, vertex_color):
|
|
||||||
self.gltf = gltf
|
|
||||||
self.pymat = pymat
|
|
||||||
self.mat = mat
|
|
||||||
self.node_tree = mat.node_tree
|
|
||||||
self.vertex_color = vertex_color
|
|
||||||
if pymat.pbr_metallic_roughness is None:
|
|
||||||
pymat.pbr_metallic_roughness = \
|
|
||||||
MaterialPBRMetallicRoughness.from_dict({})
|
|
||||||
self.settings_node = None
|
|
||||||
|
|
||||||
def is_opaque(self):
|
|
||||||
alpha_mode = self.pymat.alpha_mode
|
|
||||||
return alpha_mode is None or alpha_mode == 'OPAQUE'
|
|
||||||
|
|
||||||
def needs_emissive(self):
|
|
||||||
return (
|
|
||||||
self.pymat.emissive_texture is not None or
|
|
||||||
(self.pymat.emissive_factor or [0, 0, 0]) != [0, 0, 0]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def pbr_metallic_roughness(mh: MaterialHelper):
|
def pbr_metallic_roughness(mh: MaterialHelper):
|
||||||
"""Creates node tree for pbrMetallicRoughness materials."""
|
"""Creates node tree for pbrMetallicRoughness materials."""
|
||||||
pbr_node = mh.node_tree.nodes.new('ShaderNodeBsdfPrincipled')
|
pbr_node = mh.nodes.new('ShaderNodeBsdfPrincipled')
|
||||||
|
out_node = mh.nodes.new('ShaderNodeOutputMaterial')
|
||||||
pbr_node.location = 10, 300
|
pbr_node.location = 10, 300
|
||||||
additional_location = 40, -370 # For occlusion and/or volume / original PBR extensions
|
out_node.location = 300, 300
|
||||||
|
mh.links.new(pbr_node.outputs[0], out_node.inputs[0])
|
||||||
|
|
||||||
# Set IOR to 1.5, this is the default in glTF
|
need_volume_node = False # need a place to attach volume?
|
||||||
# This value may be overridden later if IOR extension is set on file
|
need_settings_node = False # need a place to attach occlusion/thickness?
|
||||||
pbr_node.inputs['IOR'].default_value = GLTF_IOR
|
|
||||||
|
|
||||||
if mh.pymat.occlusion_texture is not None:
|
if mh.pymat.occlusion_texture is not None:
|
||||||
if mh.settings_node is None:
|
need_settings_node = True
|
||||||
mh.settings_node = make_settings_node(mh)
|
|
||||||
mh.settings_node.location = additional_location
|
|
||||||
mh.settings_node.width = 180
|
|
||||||
additional_location = additional_location[0], additional_location[1] - 150
|
|
||||||
|
|
||||||
need_volume_node = False
|
|
||||||
if mh.pymat.extensions and 'KHR_materials_volume' in mh.pymat.extensions:
|
|
||||||
if 'thicknessFactor' in mh.pymat.extensions['KHR_materials_volume'] \
|
|
||||||
and mh.pymat.extensions['KHR_materials_volume']['thicknessFactor'] != 0.0:
|
|
||||||
|
|
||||||
|
if volume_ext := mh.get_ext('KHR_materials_volume'):
|
||||||
|
if volume_ext.get('thicknessFactor', 0) != 0:
|
||||||
need_volume_node = True
|
need_volume_node = True
|
||||||
|
need_settings_node = True
|
||||||
|
|
||||||
# We also need glTF Material Output Node, to set thicknessFactor and thicknessTexture
|
if need_settings_node:
|
||||||
if mh.settings_node is None:
|
mh.settings_node = make_settings_node(mh)
|
||||||
mh.settings_node = make_settings_node(mh)
|
mh.settings_node.location = 40, -370
|
||||||
mh.settings_node.location = additional_location
|
mh.settings_node.width = 180
|
||||||
mh.settings_node.width = 180
|
|
||||||
additional_location = additional_location[0], additional_location[1] - 150
|
|
||||||
|
|
||||||
_, _, volume_socket = make_output_nodes(
|
|
||||||
mh,
|
|
||||||
location=(250, 260),
|
|
||||||
additional_location=additional_location,
|
|
||||||
shader_socket=pbr_node.outputs[0],
|
|
||||||
make_emission_socket=False, # is managed by Principled shader node
|
|
||||||
make_alpha_socket=False, # is managed by Principled shader node
|
|
||||||
make_volume_socket=need_volume_node
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if need_volume_node:
|
||||||
|
volume_node = mh.nodes.new('ShaderNodeVolumeAbsorption')
|
||||||
|
volume_node.location = 40, -520 if need_settings_node else -370
|
||||||
|
mh.links.new(out_node.inputs[1], volume_node.outputs[0])
|
||||||
|
|
||||||
locs = calc_locations(mh)
|
locs = calc_locations(mh)
|
||||||
|
|
||||||
@ -119,45 +78,19 @@ def pbr_metallic_roughness(mh: MaterialHelper):
|
|||||||
occlusion_socket=mh.settings_node.inputs['Occlusion'],
|
occlusion_socket=mh.settings_node.inputs['Occlusion'],
|
||||||
)
|
)
|
||||||
|
|
||||||
clearcoat(
|
clearcoat(mh, locs, pbr_node)
|
||||||
mh,
|
|
||||||
location=locs['clearcoat'],
|
|
||||||
clearcoat_socket=pbr_node.inputs['Coat Weight'],
|
|
||||||
)
|
|
||||||
|
|
||||||
clearcoat_roughness(
|
transmission(mh, locs, pbr_node)
|
||||||
mh,
|
|
||||||
location=locs['clearcoat_roughness'],
|
|
||||||
roughness_socket=pbr_node.inputs['Coat Roughness'],
|
|
||||||
)
|
|
||||||
|
|
||||||
clearcoat_normal(
|
|
||||||
mh,
|
|
||||||
location=locs['clearcoat_normal'],
|
|
||||||
normal_socket=pbr_node.inputs['Coat Normal'],
|
|
||||||
)
|
|
||||||
|
|
||||||
transmission(
|
|
||||||
mh,
|
|
||||||
location=locs['transmission'],
|
|
||||||
transmission_socket=pbr_node.inputs['Transmission Weight']
|
|
||||||
)
|
|
||||||
|
|
||||||
if need_volume_node:
|
if need_volume_node:
|
||||||
volume(
|
volume(
|
||||||
mh,
|
mh,
|
||||||
location=locs['volume_thickness'],
|
location=locs['volume_thickness'],
|
||||||
volume_socket=volume_socket,
|
volume_node=volume_node,
|
||||||
thickness_socket=mh.settings_node.inputs[1] if mh.settings_node else None
|
thickness_socket=mh.settings_node.inputs[1] if mh.settings_node else None
|
||||||
)
|
)
|
||||||
|
|
||||||
specular(
|
specular(mh, locs, pbr_node)
|
||||||
mh,
|
|
||||||
location_specular=locs['specularTexture'],
|
|
||||||
location_specular_tint=locs['specularColorTexture'],
|
|
||||||
specular_socket=pbr_node.inputs['Specular IOR Level'],
|
|
||||||
specular_tint_socket=pbr_node.inputs['Specular Tint']
|
|
||||||
)
|
|
||||||
|
|
||||||
anisotropy(
|
anisotropy(
|
||||||
mh,
|
mh,
|
||||||
@ -167,18 +100,135 @@ def pbr_metallic_roughness(mh: MaterialHelper):
|
|||||||
anisotropy_tangent_socket=pbr_node.inputs['Tangent']
|
anisotropy_tangent_socket=pbr_node.inputs['Tangent']
|
||||||
)
|
)
|
||||||
|
|
||||||
sheen(
|
sheen(mh, locs, pbr_node)
|
||||||
|
|
||||||
|
# IOR
|
||||||
|
ior_ext = mh.get_ext('KHR_materials_ior', {})
|
||||||
|
ior = ior_ext.get('ior', GLTF_IOR)
|
||||||
|
pbr_node.inputs['IOR'].default_value = ior
|
||||||
|
|
||||||
|
|
||||||
|
def clearcoat(mh, locs, pbr_node):
|
||||||
|
ext = mh.get_ext('KHR_materials_clearcoat', {})
|
||||||
|
|
||||||
|
scalar_factor_and_texture(
|
||||||
mh,
|
mh,
|
||||||
location_sheenTint=locs['sheenColorTexture'],
|
location=locs['clearcoat'],
|
||||||
location_sheenRoughness=locs['sheenRoughnessTexture'],
|
label='Clearcoat',
|
||||||
sheen_socket=pbr_node.inputs['Sheen Weight'],
|
socket=pbr_node.inputs['Coat Weight'],
|
||||||
sheenTint_socket=pbr_node.inputs['Sheen Tint'],
|
factor=ext.get('clearcoatFactor', 0),
|
||||||
sheenRoughness_socket=pbr_node.inputs['Sheen Roughness']
|
tex_info=ext.get('clearcoatTexture'),
|
||||||
|
channel=0, # Red
|
||||||
)
|
)
|
||||||
|
|
||||||
ior(
|
scalar_factor_and_texture(
|
||||||
mh,
|
mh,
|
||||||
ior_socket=pbr_node.inputs['IOR']
|
location=locs['clearcoat_roughness'],
|
||||||
|
label='Clearcoat Roughness',
|
||||||
|
socket=pbr_node.inputs['Coat Roughness'],
|
||||||
|
factor=ext.get('clearcoatRoughnessFactor', 0),
|
||||||
|
tex_info=ext.get('clearcoatRoughnessTexture'),
|
||||||
|
channel=1, # Green
|
||||||
|
)
|
||||||
|
|
||||||
|
normal_map(
|
||||||
|
mh,
|
||||||
|
location=locs['clearcoat_normal'],
|
||||||
|
label='Clearcoat Normal',
|
||||||
|
socket=pbr_node.inputs['Coat Normal'],
|
||||||
|
tex_info=ext.get('clearcoatNormalTexture'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def transmission(mh, locs, pbr_node):
|
||||||
|
ext = mh.get_ext('KHR_materials_transmission', {})
|
||||||
|
factor = ext.get('transmissionFactor', 0)
|
||||||
|
|
||||||
|
if factor > 0:
|
||||||
|
# Activate screen refraction (for Eevee)
|
||||||
|
mh.mat.use_screen_refraction = True
|
||||||
|
|
||||||
|
scalar_factor_and_texture(
|
||||||
|
mh,
|
||||||
|
location=locs['transmission'],
|
||||||
|
label='Transmission',
|
||||||
|
socket=pbr_node.inputs['Transmission Weight'],
|
||||||
|
factor=factor,
|
||||||
|
tex_info=ext.get('transmissionTexture'),
|
||||||
|
channel=0, # Red
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def volume(mh, location, volume_node, thickness_socket):
|
||||||
|
# Based on https://github.com/KhronosGroup/glTF-Blender-IO/issues/1454#issuecomment-928319444
|
||||||
|
ext = mh.get_ext('KHR_materials_volume', {})
|
||||||
|
|
||||||
|
color = ext.get('attenuationColor', [1, 1, 1])
|
||||||
|
volume_node.inputs[0].default_value = [*color, 1]
|
||||||
|
|
||||||
|
distance = ext.get('attenuationDistance', float('inf'))
|
||||||
|
density = 1 / distance
|
||||||
|
volume_node.inputs[1].default_value = density
|
||||||
|
|
||||||
|
scalar_factor_and_texture(
|
||||||
|
mh,
|
||||||
|
location=location,
|
||||||
|
label='Thickness',
|
||||||
|
socket=thickness_socket,
|
||||||
|
factor=ext.get('thicknessFactor', 0),
|
||||||
|
tex_info=ext.get('thicknessTexture'),
|
||||||
|
channel=1, # Green
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def specular(mh, locs, pbr_node):
|
||||||
|
ext = mh.get_ext('KHR_materials_specular', {})
|
||||||
|
|
||||||
|
# blender.IORLevel = 0.5 * gltf.specular
|
||||||
|
scalar_factor_and_texture(
|
||||||
|
mh,
|
||||||
|
location=locs['specularTexture'],
|
||||||
|
label='Specular',
|
||||||
|
socket=pbr_node.inputs['Specular IOR Level'],
|
||||||
|
factor=0.5 * ext.get('specularFactor', 1),
|
||||||
|
tex_info=ext.get('specularTexture'),
|
||||||
|
channel=4, # Alpha
|
||||||
|
)
|
||||||
|
|
||||||
|
color_factor_and_texture(
|
||||||
|
mh,
|
||||||
|
location=locs['specularColorTexture'],
|
||||||
|
label='Specular Color',
|
||||||
|
socket=pbr_node.inputs['Specular Tint'],
|
||||||
|
factor=ext.get('specularColorFactor', [1, 1, 1]),
|
||||||
|
tex_info=ext.get('specularColorTexture'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sheen(mh, locs, pbr_node):
|
||||||
|
ext = mh.get_ext('KHR_materials_sheen')
|
||||||
|
if ext is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
pbr_node.inputs['Sheen Weight'].default_value = 1
|
||||||
|
|
||||||
|
color_factor_and_texture(
|
||||||
|
mh,
|
||||||
|
location=locs['sheenColorTexture'],
|
||||||
|
label='Sheen Color',
|
||||||
|
socket=pbr_node.inputs['Sheen Tint'],
|
||||||
|
factor=ext.get('sheenColorFactor', [0, 0, 0]),
|
||||||
|
tex_info=ext.get('sheenColorTexture'),
|
||||||
|
)
|
||||||
|
|
||||||
|
scalar_factor_and_texture(
|
||||||
|
mh,
|
||||||
|
location=locs['sheenRoughnessTexture'],
|
||||||
|
label='Sheen Roughness',
|
||||||
|
socket=pbr_node.inputs['Sheen Roughness'],
|
||||||
|
factor=ext.get('sheenRoughnessFactor', 0),
|
||||||
|
tex_info=ext.get('sheenRoughnessTexture'),
|
||||||
|
channel=4, # Alpha
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -190,35 +240,12 @@ def calc_locations(mh):
|
|||||||
height = 460 # height of each block
|
height = 460 # height of each block
|
||||||
locs = {}
|
locs = {}
|
||||||
|
|
||||||
try:
|
clearcoat_ext = mh.get_ext('KHR_materials_clearcoat', {})
|
||||||
clearcoat_ext = mh.pymat.extensions['KHR_materials_clearcoat']
|
transmission_ext = mh.get_ext('KHR_materials_transmission', {})
|
||||||
except Exception:
|
volume_ext = mh.get_ext('KHR_materials_volume', {})
|
||||||
clearcoat_ext = {}
|
specular_ext = mh.get_ext('KHR_materials_specular', {})
|
||||||
|
anisotropy_ext = mh.get_ext('KHR_materials_anisotropy', {})
|
||||||
try:
|
sheen_ext = mh.get_ext('KHR_materials_sheen', {})
|
||||||
transmission_ext = mh.pymat.exntesions['KHR_materials_transmission']
|
|
||||||
except:
|
|
||||||
transmission_ext = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
volume_ext = mh.pymat.extensions['KHR_materials_volume']
|
|
||||||
except Exception:
|
|
||||||
volume_ext = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
specular_ext = mh.pymat.extensions['KHR_materials_specular']
|
|
||||||
except:
|
|
||||||
specular_ext = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
anisotropy_ext = mh.pymat.extensions['KHR_materials_anisotropy']
|
|
||||||
except:
|
|
||||||
anisotropy_ext = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
sheen_ext = mh.pymat.extensions['KHR_materials_sheen']
|
|
||||||
except:
|
|
||||||
sheen_ext = {}
|
|
||||||
|
|
||||||
locs['base_color'] = (x, y)
|
locs['base_color'] = (x, y)
|
||||||
if mh.pymat.pbr_metallic_roughness.base_color_texture is not None or mh.vertex_color:
|
if mh.pymat.pbr_metallic_roughness.base_color_texture is not None or mh.vertex_color:
|
||||||
@ -283,60 +310,24 @@ def calc_locations(mh):
|
|||||||
|
|
||||||
# [Texture] => [Emissive Factor] =>
|
# [Texture] => [Emissive Factor] =>
|
||||||
def emission(mh: MaterialHelper, location, color_socket, strength_socket):
|
def emission(mh: MaterialHelper, location, color_socket, strength_socket):
|
||||||
x, y = location
|
factor = mh.pymat.emissive_factor or [0, 0, 0]
|
||||||
emissive_factor = mh.pymat.emissive_factor or [0, 0, 0]
|
ext = mh.get_ext('KHR_materials_emissive_strength', {})
|
||||||
|
strength = ext.get('emissiveStrength', 1)
|
||||||
|
|
||||||
strength = 1
|
if factor[0] == factor[1] == factor[2]:
|
||||||
try:
|
# Fold greyscale factor into strength
|
||||||
# Get strength from KHR_materials_emissive_strength if exists
|
strength *= factor[0]
|
||||||
strength = mh.pymat.extensions['KHR_materials_emissive_strength']['emissiveStrength']
|
factor = [1, 1, 1]
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if color_socket is None:
|
color_factor_and_texture(
|
||||||
return
|
|
||||||
|
|
||||||
if mh.pymat.emissive_texture is None:
|
|
||||||
if emissive_factor == [0, 0, 0]:
|
|
||||||
# Keep as close as possible to the default Blender value when there is no emission
|
|
||||||
color_socket.default_value = [1,1,1,1]
|
|
||||||
strength_socket.default_value = 0
|
|
||||||
return
|
|
||||||
color_socket.default_value = emissive_factor + [1]
|
|
||||||
strength_socket.default_value = strength
|
|
||||||
return
|
|
||||||
|
|
||||||
# Put grayscale emissive factors into the Emission Strength
|
|
||||||
e0, e1, e2 = emissive_factor
|
|
||||||
if strength_socket and e0 == e1 == e2:
|
|
||||||
strength_socket.default_value = e0 * strength
|
|
||||||
|
|
||||||
# Otherwise, use a multiply node for it
|
|
||||||
else:
|
|
||||||
if emissive_factor != [1, 1, 1]:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMix')
|
|
||||||
node.label = 'Emissive Factor'
|
|
||||||
node.data_type = 'RGBA'
|
|
||||||
node.location = x - 140, y
|
|
||||||
node.blend_type = 'MULTIPLY'
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(color_socket, node.outputs[2])
|
|
||||||
# Inputs
|
|
||||||
node.inputs['Factor'].default_value = 1.0
|
|
||||||
color_socket = node.inputs[6]
|
|
||||||
node.inputs[7].default_value = emissive_factor + [1]
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
strength_socket.default_value = strength
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
mh,
|
||||||
|
location,
|
||||||
|
label='Emissive',
|
||||||
|
socket=color_socket,
|
||||||
|
factor=factor,
|
||||||
tex_info=mh.pymat.emissive_texture,
|
tex_info=mh.pymat.emissive_texture,
|
||||||
label='EMISSIVE',
|
|
||||||
location=(x, y),
|
|
||||||
color_socket=color_socket,
|
|
||||||
)
|
)
|
||||||
|
strength_socket.default_value = strength
|
||||||
|
|
||||||
|
|
||||||
# [Texture] => [Mix Colors] => [Color Factor] =>
|
# [Texture] => [Mix Colors] => [Color Factor] =>
|
||||||
@ -395,7 +386,7 @@ def base_color(
|
|||||||
if needs_alpha_factor:
|
if needs_alpha_factor:
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
||||||
node.label = 'Alpha Factor'
|
node.label = 'Alpha Factor'
|
||||||
node.location = x - 140, y - 200
|
node.location = x - 140, y - 230
|
||||||
# Outputs
|
# Outputs
|
||||||
mh.node_tree.links.new(alpha_socket, node.outputs[0])
|
mh.node_tree.links.new(alpha_socket, node.outputs[0])
|
||||||
# Inputs
|
# Inputs
|
||||||
@ -428,7 +419,7 @@ def base_color(
|
|||||||
if alpha_socket is not None:
|
if alpha_socket is not None:
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
node = mh.node_tree.nodes.new('ShaderNodeMath')
|
||||||
node.label = 'Mix Vertex Alpha'
|
node.label = 'Mix Vertex Alpha'
|
||||||
node.location = x - 140, y - 200
|
node.location = x - 140, y - 230
|
||||||
node.operation = 'MULTIPLY'
|
node.operation = 'MULTIPLY'
|
||||||
# Outputs
|
# Outputs
|
||||||
mh.node_tree.links.new(alpha_socket, node.outputs[0])
|
mh.node_tree.links.new(alpha_socket, node.outputs[0])
|
||||||
@ -528,40 +519,12 @@ def metallic_roughness(mh: MaterialHelper, location, metallic_socket, roughness_
|
|||||||
|
|
||||||
# [Texture] => [Normal Map] =>
|
# [Texture] => [Normal Map] =>
|
||||||
def normal(mh: MaterialHelper, location, normal_socket):
|
def normal(mh: MaterialHelper, location, normal_socket):
|
||||||
x,y = location
|
normal_map(
|
||||||
tex_info = mh.pymat.normal_texture
|
|
||||||
|
|
||||||
if tex_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Normal map
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeNormalMap')
|
|
||||||
node.location = x - 150, y - 40
|
|
||||||
# Set UVMap
|
|
||||||
uv_idx = tex_info.tex_coord or 0
|
|
||||||
try:
|
|
||||||
uv_idx = tex_info.extensions['KHR_texture_transform']['texCoord']
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
node.uv_map = 'UVMap' if uv_idx == 0 else 'UVMap.%03d' % uv_idx
|
|
||||||
# Set strength
|
|
||||||
scale = tex_info.scale
|
|
||||||
scale = scale if scale is not None else 1
|
|
||||||
node.inputs['Strength'].default_value = scale
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(normal_socket, node.outputs['Normal'])
|
|
||||||
# Inputs
|
|
||||||
color_socket = node.inputs['Color']
|
|
||||||
|
|
||||||
x -= 200
|
|
||||||
|
|
||||||
texture(
|
|
||||||
mh,
|
mh,
|
||||||
tex_info=tex_info,
|
location=location,
|
||||||
label='NORMALMAP',
|
label='Normal Map',
|
||||||
location=(x, y),
|
socket=normal_socket,
|
||||||
is_data=True,
|
tex_info=mh.pymat.normal_texture,
|
||||||
color_socket=color_socket,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -610,98 +573,6 @@ def occlusion(mh: MaterialHelper, location, occlusion_socket):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# => [Add Emission] => [Mix Alpha] => [Material Output] if needed, only for SpecGlossiness
|
|
||||||
# => [Volume] => [Add Shader] => [Material Output] if needed
|
|
||||||
# => [Sheen] => [Add Shader] => [Material Output] if needed
|
|
||||||
def make_output_nodes(
|
|
||||||
mh: MaterialHelper,
|
|
||||||
location,
|
|
||||||
additional_location,
|
|
||||||
shader_socket,
|
|
||||||
make_emission_socket,
|
|
||||||
make_alpha_socket,
|
|
||||||
make_volume_socket
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Creates the Material Output node and connects shader_socket to it.
|
|
||||||
If requested, it can also create places to hookup the emission/alpha
|
|
||||||
in between shader_socket and the Output node too.
|
|
||||||
|
|
||||||
:return: a pair containing the sockets you should put emission and alpha
|
|
||||||
in (None if not requested).
|
|
||||||
"""
|
|
||||||
x, y = location
|
|
||||||
emission_socket = None
|
|
||||||
alpha_socket = None
|
|
||||||
|
|
||||||
# Create an Emission node and add it to the shader.
|
|
||||||
if make_emission_socket:
|
|
||||||
# Emission
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeEmission')
|
|
||||||
node.location = x + 50, y + 250
|
|
||||||
# Inputs
|
|
||||||
emission_socket = node.inputs[0]
|
|
||||||
# Outputs
|
|
||||||
emission_output = node.outputs[0]
|
|
||||||
|
|
||||||
# Add
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeAddShader')
|
|
||||||
node.location = x + 250, y + 160
|
|
||||||
# Inputs
|
|
||||||
mh.node_tree.links.new(node.inputs[0], emission_output)
|
|
||||||
mh.node_tree.links.new(node.inputs[1], shader_socket)
|
|
||||||
# Outputs
|
|
||||||
shader_socket = node.outputs[0]
|
|
||||||
|
|
||||||
if make_alpha_socket:
|
|
||||||
x += 200
|
|
||||||
y += 175
|
|
||||||
else:
|
|
||||||
x += 380
|
|
||||||
y += 125
|
|
||||||
|
|
||||||
# Mix with a Transparent BSDF. Mixing factor is the alpha value.
|
|
||||||
if make_alpha_socket:
|
|
||||||
# Transparent BSDF
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeBsdfTransparent')
|
|
||||||
node.location = x + 100, y - 350
|
|
||||||
# Outputs
|
|
||||||
transparent_out = node.outputs[0]
|
|
||||||
|
|
||||||
# Mix
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeMixShader')
|
|
||||||
node.location = x + 340, y - 180
|
|
||||||
# Inputs
|
|
||||||
alpha_socket = node.inputs[0]
|
|
||||||
mh.node_tree.links.new(node.inputs[1], transparent_out)
|
|
||||||
mh.node_tree.links.new(node.inputs[2], shader_socket)
|
|
||||||
# Outputs
|
|
||||||
shader_socket = node.outputs[0]
|
|
||||||
|
|
||||||
|
|
||||||
x += 480
|
|
||||||
y -= 210
|
|
||||||
|
|
||||||
# Material output
|
|
||||||
node_output = mh.node_tree.nodes.new('ShaderNodeOutputMaterial')
|
|
||||||
node_output.location = x + 70, y + 10
|
|
||||||
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(node_output.inputs[0], shader_socket)
|
|
||||||
|
|
||||||
# Volume Node
|
|
||||||
volume_socket = None
|
|
||||||
if make_volume_socket:
|
|
||||||
node = mh.node_tree.nodes.new('ShaderNodeVolumeAbsorption')
|
|
||||||
node.location = additional_location
|
|
||||||
# Outputs
|
|
||||||
mh.node_tree.links.new(node_output.inputs[1], node.outputs[0])
|
|
||||||
volume_socket = node.outputs[0]
|
|
||||||
|
|
||||||
|
|
||||||
return emission_socket, alpha_socket, volume_socket
|
|
||||||
|
|
||||||
|
|
||||||
def make_settings_node(mh):
|
def make_settings_node(mh):
|
||||||
"""
|
"""
|
||||||
Make a Group node with a hookup for Occlusion. No effect in Blender, but
|
Make a Group node with a hookup for Occlusion. No effect in Blender, but
|
||||||
|
@ -23,8 +23,6 @@ class BlenderScene():
|
|||||||
gltf.blender_scene = scene.name
|
gltf.blender_scene = scene.name
|
||||||
if bpy.context.collection.name in bpy.data.collections: # avoid master collection
|
if bpy.context.collection.name in bpy.data.collections: # avoid master collection
|
||||||
gltf.blender_active_collection = bpy.context.collection.name
|
gltf.blender_active_collection = bpy.context.collection.name
|
||||||
if scene.render.engine not in ['CYCLES', 'BLENDER_EEVEE']:
|
|
||||||
scene.render.engine = "BLENDER_EEVEE"
|
|
||||||
|
|
||||||
if gltf.data.scene is not None:
|
if gltf.data.scene is not None:
|
||||||
import_user_extensions('gather_import_scene_before_hook', gltf, gltf.data.scenes[gltf.data.scene], scene)
|
import_user_extensions('gather_import_scene_before_hook', gltf, gltf.data.scenes[gltf.data.scene], scene)
|
||||||
|
@ -39,25 +39,7 @@ def texture(
|
|||||||
|
|
||||||
# Get image
|
# Get image
|
||||||
if forced_image is None:
|
if forced_image is None:
|
||||||
|
source = get_source(mh, pytexture)
|
||||||
if mh.gltf.import_settings['import_webp_texture'] is True:
|
|
||||||
# Get the WebP image if there is one
|
|
||||||
if pytexture.extensions \
|
|
||||||
and 'EXT_texture_webp' in pytexture.extensions \
|
|
||||||
and pytexture.extensions['EXT_texture_webp']['source'] is not None:
|
|
||||||
source = pytexture.extensions['EXT_texture_webp']['source']
|
|
||||||
elif pytexture.source is not None:
|
|
||||||
source = pytexture.source
|
|
||||||
else:
|
|
||||||
source = pytexture.source
|
|
||||||
|
|
||||||
if mh.gltf.import_settings['import_webp_texture'] is False and source is None:
|
|
||||||
# In case webp is not used as a fallback, use this as main texture
|
|
||||||
if pytexture.extensions \
|
|
||||||
and 'EXT_texture_webp' in pytexture.extensions \
|
|
||||||
and pytexture.extensions['EXT_texture_webp']['source'] is not None:
|
|
||||||
source = pytexture.extensions['EXT_texture_webp']['source']
|
|
||||||
|
|
||||||
if source is not None:
|
if source is not None:
|
||||||
BlenderImage.create(mh.gltf, source)
|
BlenderImage.create(mh.gltf, source)
|
||||||
pyimg = mh.gltf.data.images[source]
|
pyimg = mh.gltf.data.images[source]
|
||||||
@ -188,6 +170,20 @@ def texture(
|
|||||||
|
|
||||||
import_user_extensions('gather_import_texture_after_hook', mh.gltf, pytexture, mh.node_tree, mh, tex_info, location, label, color_socket, alpha_socket, is_data)
|
import_user_extensions('gather_import_texture_after_hook', mh.gltf, pytexture, mh.node_tree, mh, tex_info, location, label, color_socket, alpha_socket, is_data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_source(mh, pytexture):
|
||||||
|
src = pytexture.source
|
||||||
|
try:
|
||||||
|
webp_src = pytexture.extensions['EXT_texture_webp']['source']
|
||||||
|
except Exception:
|
||||||
|
webp_src = None
|
||||||
|
|
||||||
|
if mh.gltf.import_settings['import_webp_texture']:
|
||||||
|
return webp_src if webp_src is not None else src
|
||||||
|
else:
|
||||||
|
return src if src is not None else webp_src
|
||||||
|
|
||||||
|
|
||||||
def set_filtering(tex_img, pysampler):
|
def set_filtering(tex_img, pysampler):
|
||||||
"""Set the filtering/interpolation on an Image Texture from the glTf sampler."""
|
"""Set the filtering/interpolation on an Image Texture from the glTf sampler."""
|
||||||
minf = pysampler.min_filter
|
minf = pysampler.min_filter
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Node Wrangler",
|
"name": "Node Wrangler",
|
||||||
"author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
|
"author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
|
||||||
"version": (3, 51),
|
"version": (3, 52),
|
||||||
"blender": (4, 0, 0),
|
"blender": (4, 0, 0),
|
||||||
"location": "Node Editor Toolbar or Shift-W",
|
"location": "Node Editor Toolbar or Shift-W",
|
||||||
"description": "Various tools to enhance and speed up node-based workflow",
|
"description": "Various tools to enhance and speed up node-based workflow",
|
||||||
|
@ -10,7 +10,7 @@ from nodeitems_utils import node_categories_iter, NodeItemCustom
|
|||||||
from . import operators
|
from . import operators
|
||||||
|
|
||||||
from .utils.constants import blend_types, geo_combine_operations, operations
|
from .utils.constants import blend_types, geo_combine_operations, operations
|
||||||
from .utils.nodes import get_nodes_links, nw_check, NWBase
|
from .utils.nodes import get_nodes_links, NWBaseMenu
|
||||||
|
|
||||||
|
|
||||||
def drawlayout(context, layout, mode='non-panel'):
|
def drawlayout(context, layout, mode='non-panel'):
|
||||||
@ -71,7 +71,7 @@ def drawlayout(context, layout, mode='non-panel'):
|
|||||||
col.separator()
|
col.separator()
|
||||||
|
|
||||||
|
|
||||||
class NodeWranglerPanel(Panel, NWBase):
|
class NodeWranglerPanel(Panel, NWBaseMenu):
|
||||||
bl_idname = "NODE_PT_nw_node_wrangler"
|
bl_idname = "NODE_PT_nw_node_wrangler"
|
||||||
bl_space_type = 'NODE_EDITOR'
|
bl_space_type = 'NODE_EDITOR'
|
||||||
bl_label = "Node Wrangler"
|
bl_label = "Node Wrangler"
|
||||||
@ -92,7 +92,7 @@ class NodeWranglerPanel(Panel, NWBase):
|
|||||||
#
|
#
|
||||||
# M E N U S
|
# M E N U S
|
||||||
#
|
#
|
||||||
class NodeWranglerMenu(Menu, NWBase):
|
class NodeWranglerMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_node_wrangler_menu"
|
bl_idname = "NODE_MT_nw_node_wrangler_menu"
|
||||||
bl_label = "Node Wrangler"
|
bl_label = "Node Wrangler"
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ class NodeWranglerMenu(Menu, NWBase):
|
|||||||
drawlayout(context, self.layout)
|
drawlayout(context, self.layout)
|
||||||
|
|
||||||
|
|
||||||
class NWMergeNodesMenu(Menu, NWBase):
|
class NWMergeNodesMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_merge_nodes_menu"
|
bl_idname = "NODE_MT_nw_merge_nodes_menu"
|
||||||
bl_label = "Merge Selected Nodes"
|
bl_label = "Merge Selected Nodes"
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ class NWMergeNodesMenu(Menu, NWBase):
|
|||||||
props.merge_type = 'ALPHAOVER'
|
props.merge_type = 'ALPHAOVER'
|
||||||
|
|
||||||
|
|
||||||
class NWMergeGeometryMenu(Menu, NWBase):
|
class NWMergeGeometryMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_merge_geometry_menu"
|
bl_idname = "NODE_MT_nw_merge_geometry_menu"
|
||||||
bl_label = "Merge Selected Nodes using Geometry Nodes"
|
bl_label = "Merge Selected Nodes using Geometry Nodes"
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ class NWMergeGeometryMenu(Menu, NWBase):
|
|||||||
props.merge_type = 'GEOMETRY'
|
props.merge_type = 'GEOMETRY'
|
||||||
|
|
||||||
|
|
||||||
class NWMergeShadersMenu(Menu, NWBase):
|
class NWMergeShadersMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_merge_shaders_menu"
|
bl_idname = "NODE_MT_nw_merge_shaders_menu"
|
||||||
bl_label = "Merge Selected Nodes using Shaders"
|
bl_label = "Merge Selected Nodes using Shaders"
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ class NWMergeShadersMenu(Menu, NWBase):
|
|||||||
props.merge_type = 'SHADER'
|
props.merge_type = 'SHADER'
|
||||||
|
|
||||||
|
|
||||||
class NWMergeMixMenu(Menu, NWBase):
|
class NWMergeMixMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_merge_mix_menu"
|
bl_idname = "NODE_MT_nw_merge_mix_menu"
|
||||||
bl_label = "Merge Selected Nodes using Mix"
|
bl_label = "Merge Selected Nodes using Mix"
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ class NWMergeMixMenu(Menu, NWBase):
|
|||||||
props.merge_type = 'MIX'
|
props.merge_type = 'MIX'
|
||||||
|
|
||||||
|
|
||||||
class NWConnectionListOutputs(Menu, NWBase):
|
class NWConnectionListOutputs(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_connection_list_out"
|
bl_idname = "NODE_MT_nw_connection_list_out"
|
||||||
bl_label = "From:"
|
bl_label = "From:"
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ class NWConnectionListOutputs(Menu, NWBase):
|
|||||||
icon="RADIOBUT_OFF").from_socket = index
|
icon="RADIOBUT_OFF").from_socket = index
|
||||||
|
|
||||||
|
|
||||||
class NWConnectionListInputs(Menu, NWBase):
|
class NWConnectionListInputs(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_connection_list_in"
|
bl_idname = "NODE_MT_nw_connection_list_in"
|
||||||
bl_label = "To:"
|
bl_label = "To:"
|
||||||
|
|
||||||
@ -201,7 +201,7 @@ class NWConnectionListInputs(Menu, NWBase):
|
|||||||
op.to_socket = index
|
op.to_socket = index
|
||||||
|
|
||||||
|
|
||||||
class NWMergeMathMenu(Menu, NWBase):
|
class NWMergeMathMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_merge_math_menu"
|
bl_idname = "NODE_MT_nw_merge_math_menu"
|
||||||
bl_label = "Merge Selected Nodes using Math"
|
bl_label = "Merge Selected Nodes using Math"
|
||||||
|
|
||||||
@ -213,7 +213,7 @@ class NWMergeMathMenu(Menu, NWBase):
|
|||||||
props.merge_type = 'MATH'
|
props.merge_type = 'MATH'
|
||||||
|
|
||||||
|
|
||||||
class NWBatchChangeNodesMenu(Menu, NWBase):
|
class NWBatchChangeNodesMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
|
bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
|
||||||
bl_label = "Batch Change Selected Nodes"
|
bl_label = "Batch Change Selected Nodes"
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ class NWBatchChangeNodesMenu(Menu, NWBase):
|
|||||||
layout.menu(NWBatchChangeOperationMenu.bl_idname)
|
layout.menu(NWBatchChangeOperationMenu.bl_idname)
|
||||||
|
|
||||||
|
|
||||||
class NWBatchChangeBlendTypeMenu(Menu, NWBase):
|
class NWBatchChangeBlendTypeMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
|
bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
|
||||||
bl_label = "Batch Change Blend Type"
|
bl_label = "Batch Change Blend Type"
|
||||||
|
|
||||||
@ -235,7 +235,7 @@ class NWBatchChangeBlendTypeMenu(Menu, NWBase):
|
|||||||
props.operation = 'CURRENT'
|
props.operation = 'CURRENT'
|
||||||
|
|
||||||
|
|
||||||
class NWBatchChangeOperationMenu(Menu, NWBase):
|
class NWBatchChangeOperationMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_batch_change_operation_menu"
|
bl_idname = "NODE_MT_nw_batch_change_operation_menu"
|
||||||
bl_label = "Batch Change Math Operation"
|
bl_label = "Batch Change Math Operation"
|
||||||
|
|
||||||
@ -247,7 +247,7 @@ class NWBatchChangeOperationMenu(Menu, NWBase):
|
|||||||
props.operation = type
|
props.operation = type
|
||||||
|
|
||||||
|
|
||||||
class NWCopyToSelectedMenu(Menu, NWBase):
|
class NWCopyToSelectedMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_copy_node_properties_menu"
|
bl_idname = "NODE_MT_nw_copy_node_properties_menu"
|
||||||
bl_label = "Copy to Selected"
|
bl_label = "Copy to Selected"
|
||||||
|
|
||||||
@ -257,7 +257,7 @@ class NWCopyToSelectedMenu(Menu, NWBase):
|
|||||||
layout.menu(NWCopyLabelMenu.bl_idname)
|
layout.menu(NWCopyLabelMenu.bl_idname)
|
||||||
|
|
||||||
|
|
||||||
class NWCopyLabelMenu(Menu, NWBase):
|
class NWCopyLabelMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_copy_label_menu"
|
bl_idname = "NODE_MT_nw_copy_label_menu"
|
||||||
bl_label = "Copy Label"
|
bl_label = "Copy Label"
|
||||||
|
|
||||||
@ -268,7 +268,7 @@ class NWCopyLabelMenu(Menu, NWBase):
|
|||||||
layout.operator(operators.NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
|
layout.operator(operators.NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
|
||||||
|
|
||||||
|
|
||||||
class NWAddReroutesMenu(Menu, NWBase):
|
class NWAddReroutesMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_add_reroutes_menu"
|
bl_idname = "NODE_MT_nw_add_reroutes_menu"
|
||||||
bl_label = "Add Reroutes"
|
bl_label = "Add Reroutes"
|
||||||
bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
|
bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
|
||||||
@ -280,7 +280,7 @@ class NWAddReroutesMenu(Menu, NWBase):
|
|||||||
layout.operator(operators.NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
|
layout.operator(operators.NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
|
||||||
|
|
||||||
|
|
||||||
class NWLinkActiveToSelectedMenu(Menu, NWBase):
|
class NWLinkActiveToSelectedMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
|
bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
|
||||||
bl_label = "Link Active to Selected"
|
bl_label = "Link Active to Selected"
|
||||||
|
|
||||||
@ -291,7 +291,7 @@ class NWLinkActiveToSelectedMenu(Menu, NWBase):
|
|||||||
layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
|
layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
|
||||||
|
|
||||||
|
|
||||||
class NWLinkStandardMenu(Menu, NWBase):
|
class NWLinkStandardMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_link_standard_menu"
|
bl_idname = "NODE_MT_nw_link_standard_menu"
|
||||||
bl_label = "To All Selected"
|
bl_label = "To All Selected"
|
||||||
|
|
||||||
@ -307,7 +307,7 @@ class NWLinkStandardMenu(Menu, NWBase):
|
|||||||
props.use_outputs_names = False
|
props.use_outputs_names = False
|
||||||
|
|
||||||
|
|
||||||
class NWLinkUseNodeNameMenu(Menu, NWBase):
|
class NWLinkUseNodeNameMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_link_use_node_name_menu"
|
bl_idname = "NODE_MT_nw_link_use_node_name_menu"
|
||||||
bl_label = "Use Node Name/Label"
|
bl_label = "Use Node Name/Label"
|
||||||
|
|
||||||
@ -323,7 +323,7 @@ class NWLinkUseNodeNameMenu(Menu, NWBase):
|
|||||||
props.use_outputs_names = False
|
props.use_outputs_names = False
|
||||||
|
|
||||||
|
|
||||||
class NWLinkUseOutputsNamesMenu(Menu, NWBase):
|
class NWLinkUseOutputsNamesMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
|
bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
|
||||||
bl_label = "Use Outputs Names"
|
bl_label = "Use Outputs Names"
|
||||||
|
|
||||||
@ -345,7 +345,11 @@ class NWAttributeMenu(bpy.types.Menu):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return nw_check(context) and context.space_data.tree_type == 'ShaderNodeTree'
|
space = context.space_data
|
||||||
|
return (space.type == 'NODE_EDITOR'
|
||||||
|
and space.node_tree is not None
|
||||||
|
and space.node_tree.library is None
|
||||||
|
and space.tree_type == 'ShaderNodeTree')
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
l = self.layout
|
l = self.layout
|
||||||
@ -372,7 +376,7 @@ class NWAttributeMenu(bpy.types.Menu):
|
|||||||
l.label(text="No attributes on objects with this material")
|
l.label(text="No attributes on objects with this material")
|
||||||
|
|
||||||
|
|
||||||
class NWSwitchNodeTypeMenu(Menu, NWBase):
|
class NWSwitchNodeTypeMenu(Menu, NWBaseMenu):
|
||||||
bl_idname = "NODE_MT_nw_switch_node_type_menu"
|
bl_idname = "NODE_MT_nw_switch_node_type_menu"
|
||||||
bl_label = "Switch Type to..."
|
bl_label = "Switch Type to..."
|
||||||
|
|
||||||
@ -411,8 +415,11 @@ def bgreset_menu_func(self, context):
|
|||||||
|
|
||||||
|
|
||||||
def save_viewer_menu_func(self, context):
|
def save_viewer_menu_func(self, context):
|
||||||
if (nw_check(context)
|
space = context.space_data
|
||||||
and context.space_data.tree_type == 'CompositorNodeTree'
|
if (space.type == 'NODE_EDITOR'
|
||||||
|
and space.node_tree is not None
|
||||||
|
and space.node_tree.library is None
|
||||||
|
and space.tree_type == 'CompositorNodeTree'
|
||||||
and context.scene.node_tree.nodes.active
|
and context.scene.node_tree.nodes.active
|
||||||
and context.scene.node_tree.nodes.active.type == "VIEWER"):
|
and context.scene.node_tree.nodes.active.type == "VIEWER"):
|
||||||
self.layout.operator(operators.NWSaveViewer.bl_idname, icon='FILE_IMAGE')
|
self.layout.operator(operators.NWSaveViewer.bl_idname, icon='FILE_IMAGE')
|
||||||
@ -421,18 +428,22 @@ def save_viewer_menu_func(self, context):
|
|||||||
def reset_nodes_button(self, context):
|
def reset_nodes_button(self, context):
|
||||||
node_active = context.active_node
|
node_active = context.active_node
|
||||||
node_selected = context.selected_nodes
|
node_selected = context.selected_nodes
|
||||||
node_ignore = ["FRAME", "REROUTE", "GROUP"]
|
|
||||||
|
|
||||||
# Check if active node is in the selection and respective type
|
# Check if active node is in the selection, ignore some node types
|
||||||
if (len(node_selected) == 1) and node_active and node_active.select and node_active.type not in node_ignore:
|
if (len(node_selected) != 1
|
||||||
row = self.layout.row()
|
or node_active is None
|
||||||
row.operator(operators.NWResetNodes.bl_idname, text="Reset Node", icon="FILE_REFRESH")
|
or not node_active.select
|
||||||
self.layout.separator()
|
or node_active.type in {"REROUTE", "GROUP"}):
|
||||||
|
return
|
||||||
|
|
||||||
elif (len(node_selected) == 1) and node_active and node_active.select and node_active.type == "FRAME":
|
row = self.layout.row()
|
||||||
row = self.layout.row()
|
|
||||||
|
if node_active.type == "FRAME":
|
||||||
row.operator(operators.NWResetNodes.bl_idname, text="Reset Nodes in Frame", icon="FILE_REFRESH")
|
row.operator(operators.NWResetNodes.bl_idname, text="Reset Nodes in Frame", icon="FILE_REFRESH")
|
||||||
self.layout.separator()
|
else:
|
||||||
|
row.operator(operators.NWResetNodes.bl_idname, text="Reset Node", icon="FILE_REFRESH")
|
||||||
|
|
||||||
|
self.layout.separator()
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
|
@ -29,7 +29,9 @@ from .utils.draw import draw_callback_nodeoutline
|
|||||||
from .utils.paths import match_files_to_socket_names, split_into_components
|
from .utils.paths import match_files_to_socket_names, split_into_components
|
||||||
from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_nodes_links, is_viewer_socket, is_viewer_link,
|
from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_nodes_links, is_viewer_socket, is_viewer_link,
|
||||||
get_group_output_node, get_output_location, force_update, get_internal_socket, nw_check,
|
get_group_output_node, get_output_location, force_update, get_internal_socket, nw_check,
|
||||||
nw_check_space_type, NWBase, get_first_enabled_output, is_visible_socket, viewer_socket_name)
|
nw_check_not_empty, nw_check_selected, nw_check_active, nw_check_space_type,
|
||||||
|
nw_check_node_type, nw_check_visible_outputs, nw_check_viewer_node, NWBase,
|
||||||
|
get_first_enabled_output, is_visible_socket, viewer_socket_name)
|
||||||
|
|
||||||
class NWLazyMix(Operator, NWBase):
|
class NWLazyMix(Operator, NWBase):
|
||||||
"""Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
|
"""Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
|
||||||
@ -37,6 +39,10 @@ class NWLazyMix(Operator, NWBase):
|
|||||||
bl_label = "Mix Nodes"
|
bl_label = "Mix Nodes"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_not_empty(cls, context)
|
||||||
|
|
||||||
def modal(self, context, event):
|
def modal(self, context, event):
|
||||||
context.area.tag_redraw()
|
context.area.tag_redraw()
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
@ -115,6 +121,10 @@ class NWLazyConnect(Operator, NWBase):
|
|||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
with_menu: BoolProperty()
|
with_menu: BoolProperty()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_not_empty(cls, context)
|
||||||
|
|
||||||
def modal(self, context, event):
|
def modal(self, context, event):
|
||||||
context.area.tag_redraw()
|
context.area.tag_redraw()
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
@ -244,10 +254,10 @@ class NWDeleteUnused(Operator, NWBase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
"""Disabled for custom nodes as we do not know which nodes are supported."""
|
"""Disabled for custom nodes as we do not know which nodes are supported."""
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
|
and nw_check_not_empty(cls, context)
|
||||||
'TextureNodeTree', 'GeometryNodeTree')
|
and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree',
|
||||||
and context.space_data.node_tree.nodes)
|
'TextureNodeTree', 'GeometryNodeTree'}))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
@ -334,7 +344,7 @@ class NWSwapLinks(Operator, NWBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return nw_check(context) and context.selected_nodes and len(context.selected_nodes) <= 2
|
return nw_check(cls, context) and nw_check_selected(cls, context, max=2)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
@ -448,8 +458,7 @@ class NWResetBG(Operator, NWBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return nw_check(cls, context) and nw_check_space_type(cls, context, {'CompositorNodeTree'})
|
||||||
and nw_check_space_type(cls, context, 'CompositorNodeTree'))
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
context.space_data.backdrop_zoom = 1
|
context.space_data.backdrop_zoom = 1
|
||||||
@ -468,8 +477,7 @@ class NWAddAttrNode(Operator, NWBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return nw_check(cls, context) and nw_check_space_type(cls, context, {'ShaderNodeTree'})
|
||||||
and nw_check_space_type(cls, context, 'ShaderNodeTree'))
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
|
bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
|
||||||
@ -495,10 +503,8 @@ class NWPreviewNode(Operator, NWBase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
"""Already implemented natively for compositing nodes."""
|
"""Already implemented natively for compositing nodes."""
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and nw_check_space_type(cls, context, 'ShaderNodeTree', 'GeometryNodeTree')
|
and nw_check_space_type(cls, context, {'ShaderNodeTree', 'GeometryNodeTree'}))
|
||||||
and (not context.active_node
|
|
||||||
or context.active_node.type not in {"OUTPUT_MATERIAL", "OUTPUT_WORLD"}))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_output_sockets(node_tree):
|
def get_output_sockets(node_tree):
|
||||||
@ -759,6 +765,12 @@ class NWPreviewNode(Operator, NWBase):
|
|||||||
active_node_socket_index = self.get_output_index(
|
active_node_socket_index = self.get_output_index(
|
||||||
active, output_node, base_node_tree == active_tree, 'SHADER'
|
active, output_node, base_node_tree == active_tree, 'SHADER'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Cancel if no socket was found. This can happen for group input
|
||||||
|
# nodes with only a virtual socket output.
|
||||||
|
if active_node_socket_index is None:
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
if active.outputs[active_node_socket_index].name == "Volume":
|
if active.outputs[active_node_socket_index].name == "Volume":
|
||||||
output_node_socket_index = 1
|
output_node_socket_index = 1
|
||||||
else:
|
else:
|
||||||
@ -839,11 +851,10 @@ class NWReloadImages(Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
"""Disabled for custom nodes."""
|
||||||
and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
|
return (nw_check(cls, context)
|
||||||
'TextureNodeTree', 'GeometryNodeTree')
|
and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree',
|
||||||
and context.active_node is not None
|
'TextureNodeTree', 'GeometryNodeTree'}))
|
||||||
and any(is_visible_socket(out) for out in context.active_node.outputs))
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
@ -969,9 +980,10 @@ class NWMergeNodes(Operator, NWBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
|
and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree',
|
||||||
'TextureNodeTree', 'GeometryNodeTree'))
|
'TextureNodeTree', 'GeometryNodeTree'})
|
||||||
|
and nw_check_selected(cls, context))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
settings = context.preferences.addons[__package__].preferences
|
settings = context.preferences.addons[__package__].preferences
|
||||||
@ -1292,9 +1304,10 @@ class NWBatchChangeNodes(Operator, NWBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
|
and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree',
|
||||||
'TextureNodeTree', 'GeometryNodeTree'))
|
'TextureNodeTree', 'GeometryNodeTree'})
|
||||||
|
and nw_check_selected(cls, context))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
blend_type = self.blend_type
|
blend_type = self.blend_type
|
||||||
@ -1348,6 +1361,10 @@ class NWChangeMixFactor(Operator, NWBase):
|
|||||||
bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
|
bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_selected(cls, context)
|
||||||
|
|
||||||
# option: Change factor.
|
# option: Change factor.
|
||||||
# If option is 1.0 or 0.0 - set to 1.0 or 0.0
|
# If option is 1.0 or 0.0 - set to 1.0 or 0.0
|
||||||
# Else - change factor by option value.
|
# Else - change factor by option value.
|
||||||
@ -1381,24 +1398,15 @@ class NWCopySettings(Operator, NWBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and context.active_node is not None
|
and nw_check_active(cls, context)
|
||||||
and context.active_node.type != 'FRAME')
|
and nw_check_selected(cls, context, min=2)
|
||||||
|
and nw_check_node_type(cls, context, 'FRAME', invert=True))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
node_active = context.active_node
|
node_active = context.active_node
|
||||||
node_selected = context.selected_nodes
|
node_selected = context.selected_nodes
|
||||||
|
|
||||||
# Error handling
|
|
||||||
if not (len(node_selected) > 1):
|
|
||||||
self.report({'ERROR'}, "2 nodes must be selected at least")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# Check if active node is in the selection
|
|
||||||
selected_node_names = [n.name for n in node_selected]
|
selected_node_names = [n.name for n in node_selected]
|
||||||
if node_active.name not in selected_node_names:
|
|
||||||
self.report({'ERROR'}, "No active node")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# Get nodes in selection by type
|
# Get nodes in selection by type
|
||||||
valid_nodes = [n for n in node_selected if n.type == node_active.type]
|
valid_nodes = [n for n in node_selected if n.type == node_active.type]
|
||||||
@ -1474,7 +1482,7 @@ class NWCopySettings(Operator, NWBase):
|
|||||||
new_node.location = node_loc
|
new_node.location = node_loc
|
||||||
|
|
||||||
for str_from, str_to in reconnections:
|
for str_from, str_to in reconnections:
|
||||||
node_tree.connect_sockets(eval(str_from), eval(str_to))
|
connect_sockets(eval(str_from), eval(str_to))
|
||||||
|
|
||||||
success_names.append(new_node.name)
|
success_names.append(new_node.name)
|
||||||
|
|
||||||
@ -1492,6 +1500,7 @@ class NWCopyLabel(Operator, NWBase):
|
|||||||
bl_idname = "node.nw_copy_label"
|
bl_idname = "node.nw_copy_label"
|
||||||
bl_label = "Copy Label"
|
bl_label = "Copy Label"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
bl_description = "Copy label from active to selected nodes"
|
||||||
|
|
||||||
option: EnumProperty(
|
option: EnumProperty(
|
||||||
name="option",
|
name="option",
|
||||||
@ -1503,6 +1512,10 @@ class NWCopyLabel(Operator, NWBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_selected(cls, context, min=2)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
option = self.option
|
option = self.option
|
||||||
@ -1536,9 +1549,14 @@ class NWClearLabel(Operator, NWBase):
|
|||||||
bl_idname = "node.nw_clear_label"
|
bl_idname = "node.nw_clear_label"
|
||||||
bl_label = "Clear Label"
|
bl_label = "Clear Label"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
bl_description = "Clear labels on selected nodes"
|
||||||
|
|
||||||
option: BoolProperty()
|
option: BoolProperty()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_selected(cls, context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
for node in [n for n in nodes if n.select]:
|
for node in [n for n in nodes if n.select]:
|
||||||
@ -1554,7 +1572,7 @@ class NWClearLabel(Operator, NWBase):
|
|||||||
|
|
||||||
|
|
||||||
class NWModifyLabels(Operator, NWBase):
|
class NWModifyLabels(Operator, NWBase):
|
||||||
"""Modify Labels of all selected nodes"""
|
"""Modify labels of all selected nodes"""
|
||||||
bl_idname = "node.nw_modify_labels"
|
bl_idname = "node.nw_modify_labels"
|
||||||
bl_label = "Modify Labels"
|
bl_label = "Modify Labels"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
@ -1572,6 +1590,10 @@ class NWModifyLabels(Operator, NWBase):
|
|||||||
name="Replace with"
|
name="Replace with"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_selected(cls, context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
for node in [n for n in nodes if n.select]:
|
for node in [n for n in nodes if n.select]:
|
||||||
@ -1599,8 +1621,9 @@ class NWAddTextureSetup(Operator, NWBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and nw_check_space_type(cls, context, 'ShaderNodeTree'))
|
and nw_check_space_type(cls, context, {'ShaderNodeTree'})
|
||||||
|
and nw_check_selected(cls, context))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
@ -1702,23 +1725,22 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and nw_check_space_type(cls, context, 'ShaderNodeTree'))
|
and nw_check_active(cls, context)
|
||||||
|
and nw_check_space_type(cls, context, {'ShaderNodeTree'})
|
||||||
|
and nw_check_node_type(cls, context, 'BSDF_PRINCIPLED'))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# Check if everything is ok
|
# Check if everything is ok
|
||||||
if not self.directory:
|
if not self.directory:
|
||||||
self.report({'INFO'}, 'No Folder Selected')
|
self.report({'INFO'}, 'No folder selected')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
if not self.files[:]:
|
if not self.files[:]:
|
||||||
self.report({'INFO'}, 'No Files Selected')
|
self.report({'INFO'}, 'No files selected')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
active_node = nodes.active
|
active_node = nodes.active
|
||||||
if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
|
|
||||||
self.report({'INFO'}, 'Select Principled BSDF')
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# Filter textures names for texturetypes in filenames
|
# Filter textures names for texturetypes in filenames
|
||||||
# [Socket Name, [abbreviations and keyword list], Filename placeholder]
|
# [Socket Name, [abbreviations and keyword list], Filename placeholder]
|
||||||
@ -1976,85 +1998,75 @@ class NWAddReroutes(Operator, NWBase):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_selected(cls, context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
tree_type = context.space_data.node_tree.type
|
nodes, _links = get_nodes_links(context)
|
||||||
option = self.option
|
post_select = [] # Nodes to be selected after execution.
|
||||||
nodes, links = get_nodes_links(context)
|
y_offset = -22.0
|
||||||
# output valid when option is 'all' or when 'loose' output has no links
|
|
||||||
valid = False
|
# Create reroutes and recreate links.
|
||||||
post_select = [] # nodes to be selected after execution
|
|
||||||
# create reroutes and recreate links
|
|
||||||
for node in [n for n in nodes if n.select]:
|
for node in [n for n in nodes if n.select]:
|
||||||
if node.outputs:
|
if not node.outputs:
|
||||||
x = node.location.x
|
continue
|
||||||
y = node.location.y
|
x = node.location.x + node.width + 20.0
|
||||||
width = node.width
|
y = node.location.y
|
||||||
# unhide 'REROUTE' nodes to avoid issues with location.y
|
new_node_reroutes = []
|
||||||
if node.type == 'REROUTE':
|
|
||||||
node.hide = False
|
# Unhide 'REROUTE' nodes to avoid issues with location.y
|
||||||
# Hack needed to calculate real width
|
if node.type == 'REROUTE':
|
||||||
if node.hide:
|
node.hide = False
|
||||||
bpy.ops.node.select_all(action='DESELECT')
|
else:
|
||||||
helper = nodes.new('NodeReroute')
|
y -= 35.0
|
||||||
helper.select = True
|
|
||||||
node.select = True
|
reroutes_count = 0 # Will be used when aligning reroutes added to hidden nodes.
|
||||||
# resize node and helper to zero. Then check locations to calculate width
|
|
||||||
bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
|
|
||||||
width = 2.0 * (helper.location.x - node.location.x)
|
|
||||||
# restore node location
|
|
||||||
node.location = x, y
|
|
||||||
# delete helper
|
|
||||||
node.select = False
|
|
||||||
# only helper is selected now
|
|
||||||
bpy.ops.node.delete()
|
|
||||||
x = node.location.x + width + 20.0
|
|
||||||
if node.type != 'REROUTE':
|
|
||||||
y -= 35.0
|
|
||||||
y_offset = -22.0
|
|
||||||
loc = x, y
|
|
||||||
reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
|
|
||||||
for out_i, output in enumerate(node.outputs):
|
for out_i, output in enumerate(node.outputs):
|
||||||
pass_used = False # initial value to be analyzed if 'R_LAYERS'
|
if output.is_unavailable:
|
||||||
# if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
|
continue
|
||||||
if node.type != 'R_LAYERS':
|
if node.type == 'R_LAYERS' and output.name != 'Alpha':
|
||||||
pass_used = True
|
# If 'R_LAYERS' check if output is used in render pass.
|
||||||
else: # if 'R_LAYERS' check if output represent used render pass
|
# If output is "Alpha", assume it's used. Not available in passes.
|
||||||
node_scene = node.scene
|
node_scene = node.scene
|
||||||
node_layer = node.layer
|
node_layer = node.layer
|
||||||
# If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
|
for rlo in rl_outputs:
|
||||||
if output.name == 'Alpha':
|
# Check entries in global 'rl_outputs' variable.
|
||||||
pass_used = True
|
if output.name in {rlo.output_name, rlo.exr_output_name}:
|
||||||
else:
|
if not getattr(node_scene.view_layers[node_layer], rlo.render_pass):
|
||||||
# check entries in global 'rl_outputs' variable
|
continue
|
||||||
for rlo in rl_outputs:
|
# Output is valid when option is 'all' or when 'loose' output has no links.
|
||||||
if output.name in {rlo.output_name, rlo.exr_output_name}:
|
valid = ((self.option == 'ALL') or
|
||||||
pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
|
(self.option == 'LOOSE' and not output.links) or
|
||||||
break
|
(self.option == 'LINKED' and output.links))
|
||||||
if pass_used:
|
if valid:
|
||||||
valid = ((option == 'ALL') or
|
# Add reroutes only if valid.
|
||||||
(option == 'LOOSE' and not output.links) or
|
n = nodes.new('NodeReroute')
|
||||||
(option == 'LINKED' and output.links))
|
nodes.active = n
|
||||||
# Add reroutes only if valid, but offset location in all cases.
|
for link in output.links:
|
||||||
if valid:
|
connect_sockets(n.outputs[0], link.to_socket)
|
||||||
n = nodes.new('NodeReroute')
|
connect_sockets(output, n.inputs[0])
|
||||||
nodes.active = n
|
n.location = x, y
|
||||||
for link in output.links:
|
new_node_reroutes.append(n)
|
||||||
connect_sockets(n.outputs[0], link.to_socket)
|
post_select.append(n)
|
||||||
connect_sockets(output, n.inputs[0])
|
if valid or not output.hide:
|
||||||
n.location = loc
|
# Offset reroutes for all outputs, except hidden ones.
|
||||||
post_select.append(n)
|
|
||||||
reroutes_count += 1
|
reroutes_count += 1
|
||||||
y += y_offset
|
y += y_offset
|
||||||
loc = x, y
|
|
||||||
# disselect the node so that after execution of script only newly created nodes are selected
|
# Nicer reroutes distribution along y when node.hide.
|
||||||
node.select = False
|
|
||||||
# nicer reroutes distribution along y when node.hide
|
|
||||||
if node.hide:
|
if node.hide:
|
||||||
y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
|
y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
|
||||||
for reroute in [r for r in nodes if r.select]:
|
for reroute in new_node_reroutes:
|
||||||
reroute.location.y -= y_translate
|
reroute.location.y -= y_translate
|
||||||
for node in post_select:
|
|
||||||
node.select = True
|
if post_select:
|
||||||
|
for node in nodes:
|
||||||
|
# Select only newly created nodes.
|
||||||
|
node.select = node in post_select
|
||||||
|
else:
|
||||||
|
# No new nodes were created.
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
@ -2071,9 +2083,9 @@ class NWLinkActiveToSelected(Operator, NWBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and context.active_node is not None
|
and nw_check_active(cls, context)
|
||||||
and context.active_node.select)
|
and nw_check_selected(cls, context, min=2))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
@ -2144,6 +2156,10 @@ class NWAlignNodes(Operator, NWBase):
|
|||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
|
margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_not_empty(cls, context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
margin = self.margin
|
margin = self.margin
|
||||||
@ -2222,6 +2238,10 @@ class NWSelectParentChildren(Operator, NWBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_selected(cls, context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
option = self.option
|
option = self.option
|
||||||
@ -2246,6 +2266,10 @@ class NWDetachOutputs(Operator, NWBase):
|
|||||||
bl_label = "Detach Outputs"
|
bl_label = "Detach Outputs"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return nw_check(cls, context) and nw_check_selected(cls, context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
selected = context.selected_nodes
|
selected = context.selected_nodes
|
||||||
@ -2271,11 +2295,11 @@ class NWLinkToOutputNode(Operator):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
"""Disabled for custom nodes as we do not know which nodes are outputs."""
|
"""Disabled for custom nodes as we do not know which nodes are outputs."""
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree',
|
and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree',
|
||||||
'TextureNodeTree', 'GeometryNodeTree')
|
'TextureNodeTree', 'GeometryNodeTree'})
|
||||||
and context.active_node is not None
|
and nw_check_active(cls, context)
|
||||||
and any(is_visible_socket(out) for out in context.active_node.outputs))
|
and nw_check_visible_outputs(cls, context))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
@ -2395,6 +2419,11 @@ class NWAddSequence(Operator, NWBase, ImportHelper):
|
|||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return (nw_check(cls, context)
|
||||||
|
and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree'}))
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.alignment = 'LEFT'
|
layout.alignment = 'LEFT'
|
||||||
@ -2507,6 +2536,11 @@ class NWAddMultipleImages(Operator, NWBase, ImportHelper):
|
|||||||
options={'HIDDEN', 'SKIP_SAVE'}
|
options={'HIDDEN', 'SKIP_SAVE'}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return (nw_check(cls, context)
|
||||||
|
and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree'}))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
nodes, links = get_nodes_links(context)
|
nodes, links = get_nodes_links(context)
|
||||||
|
|
||||||
@ -2556,8 +2590,8 @@ class NWViewerFocus(bpy.types.Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and nw_check_space_type(cls, context, 'CompositorNodeTree'))
|
and nw_check_space_type(cls, context, {'CompositorNodeTree'}))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
@ -2627,12 +2661,9 @@ class NWSaveViewer(bpy.types.Operator, ExportHelper):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (nw_check(context)
|
return (nw_check(cls, context)
|
||||||
and nw_check_space_type(cls, context, 'CompositorNodeTree')
|
and nw_check_space_type(cls, context, {'CompositorNodeTree'})
|
||||||
and any(img.source == 'VIEWER'
|
and nw_check_viewer_node(cls))
|
||||||
and img.render_slots == 0
|
|
||||||
for img in bpy.data.images)
|
|
||||||
and sum(bpy.data.images["Viewer Node"].size) > 0) # False if not connected or connected but no image
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
fp = self.filepath
|
fp = self.filepath
|
||||||
@ -2670,19 +2701,15 @@ class NWResetNodes(bpy.types.Operator):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
space = context.space_data
|
return (nw_check(cls, context)
|
||||||
return space.type == 'NODE_EDITOR'
|
and nw_check_selected(cls, context)
|
||||||
|
and nw_check_active(cls, context))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
node_active = context.active_node
|
node_active = context.active_node
|
||||||
node_selected = context.selected_nodes
|
node_selected = context.selected_nodes
|
||||||
node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
|
node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
|
||||||
|
|
||||||
# Check if one node is selected at least
|
|
||||||
if not (len(node_selected) > 0):
|
|
||||||
self.report({'ERROR'}, "1 node must be selected at least")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
active_node_name = node_active.name if node_active.select else None
|
active_node_name = node_active.name if node_active.select else None
|
||||||
valid_nodes = [n for n in node_selected if n.type not in node_ignore]
|
valid_nodes = [n for n in node_selected if n.type not in node_ignore]
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy_extras.node_utils import connect_sockets
|
from bpy_extras.node_utils import connect_sockets
|
||||||
from math import hypot
|
from math import hypot, inf
|
||||||
|
|
||||||
|
|
||||||
def force_update(context):
|
def force_update(context):
|
||||||
@ -200,23 +200,85 @@ def get_output_location(tree):
|
|||||||
return loc_x, loc_y
|
return loc_x, loc_y
|
||||||
|
|
||||||
|
|
||||||
def nw_check(context):
|
def nw_check(cls, context):
|
||||||
space = context.space_data
|
space = context.space_data
|
||||||
|
if space.type != 'NODE_EDITOR':
|
||||||
return (space.type == 'NODE_EDITOR'
|
cls.poll_message_set("Current editor is not a node editor.")
|
||||||
and space.node_tree is not None
|
return False
|
||||||
and space.node_tree.library is None)
|
if space.node_tree is None:
|
||||||
|
cls.poll_message_set("No node tree was found in the current node editor.")
|
||||||
|
return False
|
||||||
|
if space.node_tree.library is not None:
|
||||||
|
cls.poll_message_set("Current node tree is linked from another .blend file.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def nw_check_space_type(cls, context, *args):
|
def nw_check_not_empty(cls, context):
|
||||||
if context.space_data.tree_type not in args:
|
if not context.space_data.node_tree.nodes:
|
||||||
tree_types_str = ", ".join(t.split('NodeTree')[0].lower() for t in sorted(args))
|
cls.poll_message_set("Current node tree does not contain any nodes.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def nw_check_active(cls, context):
|
||||||
|
if context.active_node is None or not context.active_node.select:
|
||||||
|
cls.poll_message_set("No active node.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def nw_check_selected(cls, context, min=1, max=inf):
|
||||||
|
num_selected = len(context.selected_nodes)
|
||||||
|
if num_selected < min:
|
||||||
|
if min > 1:
|
||||||
|
cls.poll_message_set(f"At least {min} nodes must be selected.")
|
||||||
|
else:
|
||||||
|
cls.poll_message_set(f"At least {min} node must be selected.")
|
||||||
|
return False
|
||||||
|
if num_selected > max:
|
||||||
|
cls.poll_message_set(f"{num_selected} nodes are selected, but this operator can only work on {max}.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def nw_check_space_type(cls, context, types):
|
||||||
|
if context.space_data.tree_type not in types:
|
||||||
|
tree_types_str = ", ".join(t.split('NodeTree')[0].lower() for t in sorted(types))
|
||||||
cls.poll_message_set("Current node tree type not supported.\n"
|
cls.poll_message_set("Current node tree type not supported.\n"
|
||||||
"Should be one of " + tree_types_str + ".")
|
"Should be one of " + tree_types_str + ".")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def nw_check_node_type(cls, context, type, invert=False):
|
||||||
|
if invert and context.active_node.type == type:
|
||||||
|
cls.poll_message_set(f"Active node should be not of type {type}.")
|
||||||
|
return False
|
||||||
|
elif not invert and context.active_node.type != type:
|
||||||
|
cls.poll_message_set(f"Active node should be of type {type}.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def nw_check_visible_outputs(cls, context):
|
||||||
|
if not any(is_visible_socket(out) for out in context.active_node.outputs):
|
||||||
|
cls.poll_message_set("Current node has no visible outputs.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def nw_check_viewer_node(cls):
|
||||||
|
for img in bpy.data.images:
|
||||||
|
# False if not connected or connected but no image
|
||||||
|
if (img.source == 'VIEWER'
|
||||||
|
and len(img.render_slots) == 0
|
||||||
|
and sum(img.size) > 0):
|
||||||
|
return True
|
||||||
|
cls.poll_message_set("Viewer image not found.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_first_enabled_output(node):
|
def get_first_enabled_output(node):
|
||||||
for output in node.outputs:
|
for output in node.outputs:
|
||||||
if output.enabled:
|
if output.enabled:
|
||||||
@ -232,4 +294,13 @@ def is_visible_socket(socket):
|
|||||||
class NWBase:
|
class NWBase:
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return nw_check(context)
|
return nw_check(cls, context)
|
||||||
|
|
||||||
|
|
||||||
|
class NWBaseMenu:
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
space = context.space_data
|
||||||
|
return (space.type == 'NODE_EDITOR'
|
||||||
|
and space.node_tree is not None
|
||||||
|
and space.node_tree.library is None)
|
||||||
|
@ -15,24 +15,30 @@ bl_info = {
|
|||||||
"category": "Object"
|
"category": "Object"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if "bpy" in locals():
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
importlib.reload(carver_utils)
|
||||||
|
importlib.reload(carver_profils)
|
||||||
|
importlib.reload(carver_draw)
|
||||||
|
importlib.reload(carver_operator)
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import imp
|
|
||||||
from bpy.props import (
|
from bpy.props import (
|
||||||
BoolProperty,
|
BoolProperty,
|
||||||
StringProperty,
|
StringProperty,
|
||||||
IntProperty
|
IntProperty
|
||||||
)
|
)
|
||||||
from bpy.types import (AddonPreferences, WorkSpaceTool)
|
from bpy.types import (AddonPreferences, WorkSpaceTool)
|
||||||
from bpy.utils.toolsystem import ToolDef
|
from bpy.utils.toolsystem import ToolDef
|
||||||
|
|
||||||
from . import carver_utils
|
from . import (
|
||||||
imp.reload(carver_utils)
|
carver_utils,
|
||||||
from . import carver_profils
|
carver_profils,
|
||||||
imp.reload(carver_profils)
|
carver_draw,
|
||||||
from . import carver_draw
|
carver_operator,
|
||||||
imp.reload(carver_draw)
|
)
|
||||||
from . import carver_operator
|
|
||||||
imp.reload(carver_operator)
|
|
||||||
|
|
||||||
# TODO : Create an icon for Carver MT
|
# TODO : Create an icon for Carver MT
|
||||||
# Add an icon in the toolbar
|
# Add an icon in the toolbar
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Rigify",
|
"name": "Rigify",
|
||||||
"version": (0, 6, 9),
|
"version": (0, 6, 10),
|
||||||
"author": "Nathan Vegdahl, Lucio Rossi, Ivan Cappiello, Alexander Gavrilov", # noqa
|
"author": "Nathan Vegdahl, Lucio Rossi, Ivan Cappiello, Alexander Gavrilov", # noqa
|
||||||
"blender": (4, 0, 0),
|
"blender": (4, 0, 0),
|
||||||
"description": "Automatic rigging from building-block components",
|
"description": "Automatic rigging from building-block components",
|
||||||
@ -504,14 +504,14 @@ class RigifyBoneCollectionReference(bpy.types.PropertyGroup):
|
|||||||
|
|
||||||
arm = self.id_data.data
|
arm = self.id_data.data
|
||||||
|
|
||||||
if new_coll := arm.collections.get(new_val):
|
if new_coll := arm.collections_all.get(new_val):
|
||||||
self.set_collection(new_coll)
|
self.set_collection(new_coll)
|
||||||
else:
|
else:
|
||||||
self.find_collection(update=True)
|
self.find_collection(update=True)
|
||||||
|
|
||||||
def _name_search(self, _context, _edit):
|
def _name_search(self, _context, _edit):
|
||||||
arm = self.id_data.data
|
arm = self.id_data.data
|
||||||
return [coll.name for coll in arm.collections]
|
return [coll.name for coll in utils.misc.flatten_children(arm.collections)]
|
||||||
|
|
||||||
name: StringProperty(
|
name: StringProperty(
|
||||||
name="Collection Name", description="Name of the referenced bone collection",
|
name="Collection Name", description="Name of the referenced bone collection",
|
||||||
|
@ -17,7 +17,8 @@ from .utils.naming import (ORG_PREFIX, MCH_PREFIX, DEF_PREFIX, ROOT_NAME, make_o
|
|||||||
from .utils.widgets import WGT_PREFIX, WGT_GROUP_PREFIX
|
from .utils.widgets import WGT_PREFIX, WGT_GROUP_PREFIX
|
||||||
from .utils.widgets_special import create_root_widget
|
from .utils.widgets_special import create_root_widget
|
||||||
from .utils.mechanism import refresh_all_drivers
|
from .utils.mechanism import refresh_all_drivers
|
||||||
from .utils.misc import select_object, ArmatureObject, verify_armature_obj, choose_next_uid
|
from .utils.misc import select_object, ArmatureObject, verify_armature_obj, choose_next_uid, flatten_children,\
|
||||||
|
flatten_parents
|
||||||
from .utils.collections import (ensure_collection, list_layer_collections,
|
from .utils.collections import (ensure_collection, list_layer_collections,
|
||||||
filter_layer_collections_by_object)
|
filter_layer_collections_by_object)
|
||||||
from .utils.rig import get_rigify_type, get_rigify_target_rig,\
|
from .utils.rig import get_rigify_type, get_rigify_target_rig,\
|
||||||
@ -134,7 +135,7 @@ class Generator(base_generate.BaseGenerator):
|
|||||||
|
|
||||||
def __save_rig_data(self, obj: ArmatureObject, obj_found: bool):
|
def __save_rig_data(self, obj: ArmatureObject, obj_found: bool):
|
||||||
if obj_found:
|
if obj_found:
|
||||||
self.saved_visible_layers = {coll.name: coll.is_visible for coll in obj.data.collections}
|
self.saved_visible_layers = {coll.name: coll.is_visible for coll in obj.data.collections_all}
|
||||||
|
|
||||||
self.artifacts.generate_init_existing(obj)
|
self.artifacts.generate_init_existing(obj)
|
||||||
|
|
||||||
@ -216,14 +217,14 @@ class Generator(base_generate.BaseGenerator):
|
|||||||
self.widget_mirror_mesh[mid_name] = widget.data
|
self.widget_mirror_mesh[mid_name] = widget.data
|
||||||
|
|
||||||
def ensure_root_bone_collection(self):
|
def ensure_root_bone_collection(self):
|
||||||
collections = self.metarig.data.collections
|
collections = self.metarig.data.collections_all
|
||||||
|
|
||||||
validate_collection_references(self.metarig)
|
validate_collection_references(self.metarig)
|
||||||
|
|
||||||
coll = collections.get(ROOT_COLLECTION)
|
coll = collections.get(ROOT_COLLECTION)
|
||||||
|
|
||||||
if not coll:
|
if not coll:
|
||||||
coll = collections.new(ROOT_COLLECTION)
|
coll = self.metarig.data.collections.new(ROOT_COLLECTION)
|
||||||
|
|
||||||
if coll.rigify_ui_row <= 0:
|
if coll.rigify_ui_row <= 0:
|
||||||
coll.rigify_ui_row = 2 + choose_next_uid(collections, 'rigify_ui_row', min_value=1)
|
coll.rigify_ui_row = 2 + choose_next_uid(collections, 'rigify_ui_row', min_value=1)
|
||||||
@ -240,7 +241,7 @@ class Generator(base_generate.BaseGenerator):
|
|||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
# Remove all bone collections from the target armature.
|
# Remove all bone collections from the target armature.
|
||||||
for coll in list(obj.data.collections):
|
for coll in list(obj.data.collections_all):
|
||||||
obj.data.collections.remove(coll)
|
obj.data.collections.remove(coll)
|
||||||
|
|
||||||
# Select and duplicate metarig
|
# Select and duplicate metarig
|
||||||
@ -355,7 +356,7 @@ class Generator(base_generate.BaseGenerator):
|
|||||||
pb.lock_scale = (True, True, True)
|
pb.lock_scale = (True, True, True)
|
||||||
|
|
||||||
def ensure_bone_collection(self, name):
|
def ensure_bone_collection(self, name):
|
||||||
coll = self.obj.data.collections.get(name)
|
coll = self.obj.data.collections_all.get(name)
|
||||||
|
|
||||||
if not coll:
|
if not coll:
|
||||||
coll = self.obj.data.collections.new(name)
|
coll = self.obj.data.collections.new(name)
|
||||||
@ -435,10 +436,16 @@ class Generator(base_generate.BaseGenerator):
|
|||||||
bone.custom_shape = obj_table[wgt_name]
|
bone.custom_shape = obj_table[wgt_name]
|
||||||
|
|
||||||
def __compute_visible_layers(self):
|
def __compute_visible_layers(self):
|
||||||
|
has_ui_buttons = set().union(*[
|
||||||
|
{p.name for p in flatten_parents(coll)}
|
||||||
|
for coll in self.obj.data.collections_all
|
||||||
|
if coll.rigify_ui_row > 0
|
||||||
|
])
|
||||||
|
|
||||||
# Hide all layers without UI buttons
|
# Hide all layers without UI buttons
|
||||||
for coll in self.obj.data.collections:
|
for coll in self.obj.data.collections_all:
|
||||||
user_visible = self.saved_visible_layers.get(coll.name, coll.is_visible)
|
user_visible = self.saved_visible_layers.get(coll.name, coll.is_visible)
|
||||||
coll.is_visible = user_visible and coll.rigify_ui_row > 0
|
coll.is_visible = user_visible and coll.name in has_ui_buttons
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
context = self.context
|
context = self.context
|
||||||
@ -709,7 +716,7 @@ def create_selection_sets(obj: ArmatureObject, _metarig: ArmatureObject):
|
|||||||
|
|
||||||
obj.selection_sets.clear() # noqa
|
obj.selection_sets.clear() # noqa
|
||||||
|
|
||||||
for coll in obj.data.collections:
|
for coll in obj.data.collections_all:
|
||||||
if not coll.rigify_sel_set:
|
if not coll.rigify_sel_set:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -725,7 +732,7 @@ def apply_bone_colors(obj, metarig, priorities: Optional[dict[str, dict[str, flo
|
|||||||
|
|
||||||
collection_table: dict[str, tuple[int, 'RigifyColorSet']] = {
|
collection_table: dict[str, tuple[int, 'RigifyColorSet']] = {
|
||||||
coll.name: (i, color_map[coll.rigify_color_set_id])
|
coll.name: (i, color_map[coll.rigify_color_set_id])
|
||||||
for i, coll in enumerate(obj.data.collections)
|
for i, coll in enumerate(flatten_children(obj.data.collections))
|
||||||
if coll.rigify_color_set_id in color_map
|
if coll.rigify_color_set_id in color_map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ def create(obj): # noqa
|
|||||||
|
|
||||||
bone_collections = {}
|
bone_collections = {}
|
||||||
|
|
||||||
for bcoll in list(arm.collections):
|
for bcoll in list(arm.collections_all):
|
||||||
arm.collections.remove(bcoll)
|
arm.collections.remove(bcoll)
|
||||||
|
|
||||||
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
||||||
|
@ -48,7 +48,7 @@ def create(obj): # noqa
|
|||||||
|
|
||||||
bone_collections = {}
|
bone_collections = {}
|
||||||
|
|
||||||
for bcoll in list(arm.collections):
|
for bcoll in list(arm.collections_all):
|
||||||
arm.collections.remove(bcoll)
|
arm.collections.remove(bcoll)
|
||||||
|
|
||||||
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
||||||
|
@ -48,7 +48,7 @@ def create(obj): # noqa
|
|||||||
|
|
||||||
bone_collections = {}
|
bone_collections = {}
|
||||||
|
|
||||||
for bcoll in list(arm.collections):
|
for bcoll in list(arm.collections_all):
|
||||||
arm.collections.remove(bcoll)
|
arm.collections.remove(bcoll)
|
||||||
|
|
||||||
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
||||||
|
@ -48,7 +48,7 @@ def create(obj): # noqa
|
|||||||
|
|
||||||
bone_collections = {}
|
bone_collections = {}
|
||||||
|
|
||||||
for bcoll in list(arm.collections):
|
for bcoll in list(arm.collections_all):
|
||||||
arm.collections.remove(bcoll)
|
arm.collections.remove(bcoll)
|
||||||
|
|
||||||
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
||||||
|
@ -48,7 +48,7 @@ def create(obj): # noqa
|
|||||||
|
|
||||||
bone_collections = {}
|
bone_collections = {}
|
||||||
|
|
||||||
for bcoll in list(arm.collections):
|
for bcoll in list(arm.collections_all):
|
||||||
arm.collections.remove(bcoll)
|
arm.collections.remove(bcoll)
|
||||||
|
|
||||||
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
||||||
|
@ -48,7 +48,7 @@ def create(obj): # noqa
|
|||||||
|
|
||||||
bone_collections = {}
|
bone_collections = {}
|
||||||
|
|
||||||
for bcoll in list(arm.collections):
|
for bcoll in list(arm.collections_all):
|
||||||
arm.collections.remove(bcoll)
|
arm.collections.remove(bcoll)
|
||||||
|
|
||||||
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
||||||
|
@ -48,7 +48,7 @@ def create(obj): # noqa
|
|||||||
|
|
||||||
bone_collections = {}
|
bone_collections = {}
|
||||||
|
|
||||||
for bcoll in list(arm.collections):
|
for bcoll in list(arm.collections_all):
|
||||||
arm.collections.remove(bcoll)
|
arm.collections.remove(bcoll)
|
||||||
|
|
||||||
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
||||||
|
@ -48,7 +48,7 @@ def create(obj): # noqa
|
|||||||
|
|
||||||
bone_collections = {}
|
bone_collections = {}
|
||||||
|
|
||||||
for bcoll in list(arm.collections):
|
for bcoll in list(arm.collections_all):
|
||||||
arm.collections.remove(bcoll)
|
arm.collections.remove(bcoll)
|
||||||
|
|
||||||
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
def add_bone_collection(name, *, ui_row=0, ui_title='', sel_set=False, color_set_id=0):
|
||||||
|
@ -96,6 +96,13 @@ def ternarySearch(f, left, right, absolutePrecision):
|
|||||||
left = leftThird
|
left = leftThird
|
||||||
else:
|
else:
|
||||||
right = rightThird
|
right = rightThird
|
||||||
|
|
||||||
|
def flatten_children(iterable):
|
||||||
|
"""Enumerate the iterator items as well as their children in the tree order."""
|
||||||
|
for item in iterable:
|
||||||
|
yield item
|
||||||
|
yield from flatten_children(item.children)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
UTILITIES_FUNC_COMMON_IK_FK = ['''
|
UTILITIES_FUNC_COMMON_IK_FK = ['''
|
||||||
@ -902,7 +909,7 @@ class RigLayers(bpy.types.Panel):
|
|||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
row_table = collections.defaultdict(list)
|
row_table = collections.defaultdict(list)
|
||||||
for coll in context.active_object.data.collections:
|
for coll in flatten_children(context.active_object.data.collections):
|
||||||
row_id = coll.get('rigify_ui_row', 0)
|
row_id = coll.get('rigify_ui_row', 0)
|
||||||
if row_id > 0:
|
if row_id > 0:
|
||||||
row_table[row_id].append(coll)
|
row_table[row_id].append(coll)
|
||||||
@ -913,7 +920,9 @@ class RigLayers(bpy.types.Panel):
|
|||||||
if row_buttons:
|
if row_buttons:
|
||||||
for coll in row_buttons:
|
for coll in row_buttons:
|
||||||
title = coll.get('rigify_ui_title') or coll.name
|
title = coll.get('rigify_ui_title') or coll.name
|
||||||
row.prop(coll, 'is_visible', toggle=True, text=title)
|
row2 = row.row()
|
||||||
|
row2.active = coll.is_visible_ancestors
|
||||||
|
row2.prop(coll, 'is_visible', toggle=True, text=title)
|
||||||
else:
|
else:
|
||||||
row.separator()
|
row.separator()
|
||||||
'''
|
'''
|
||||||
|
@ -622,7 +622,7 @@ class BaseLimbRig(BaseRig):
|
|||||||
self.make_property(self.prop_bone, 'IK_Stretch', default=1.0, description='IK Stretch')
|
self.make_property(self.prop_bone, 'IK_Stretch', default=1.0, description='IK Stretch')
|
||||||
panel.custom_prop(self.prop_bone, 'IK_Stretch', text='IK Stretch', slider=True)
|
panel.custom_prop(self.prop_bone, 'IK_Stretch', text='IK Stretch', slider=True)
|
||||||
|
|
||||||
self.make_property(self.prop_bone, 'pole_vector', default=0,
|
self.make_property(self.prop_bone, 'pole_vector', default=False,
|
||||||
description='Use a pole target control')
|
description='Use a pole target control')
|
||||||
|
|
||||||
self.add_ik_only_buttons(panel, rig_name)
|
self.add_ik_only_buttons(panel, rig_name)
|
||||||
@ -1200,7 +1200,7 @@ class RigifyLimbTogglePoleBase(RigifyLimbIk2FkBase):
|
|||||||
|
|
||||||
# Set the pole property
|
# Set the pole property
|
||||||
set_custom_property_value(
|
set_custom_property_value(
|
||||||
obj, self.prop_bone, self.pole_prop, int(self.use_pole),
|
obj, self.prop_bone, self.pole_prop, bool(self.use_pole),
|
||||||
keyflags=self.keyflags_switch
|
keyflags=self.keyflags_switch
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1249,7 +1249,7 @@ class POSE_OT_rigify_limb_toggle_pole_bake(RigifyLimbTogglePoleBase, RigifyBakeK
|
|||||||
return rot_curves + pole_curves
|
return rot_curves + pole_curves
|
||||||
|
|
||||||
def execute_before_apply(self, context, obj, range, range_raw):
|
def execute_before_apply(self, context, obj, range, range_raw):
|
||||||
self.bake_replace_custom_prop_keys_constant(self.prop_bone, self.pole_prop, int(self.use_pole))
|
self.bake_replace_custom_prop_keys_constant(self.prop_bone, self.pole_prop, bool(self.use_pole))
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
self.layout.prop(self, 'use_pole')
|
self.layout.prop(self, 'use_pole')
|
||||||
@ -1272,9 +1272,10 @@ def add_limb_toggle_pole(panel: 'PanelLayout', *,
|
|||||||
}
|
}
|
||||||
|
|
||||||
row = panel.row(align=True)
|
row = panel.row(align=True)
|
||||||
left_split = row.split(factor=0.75, align=True)
|
left_split = row.split(factor=0.65, align=True)
|
||||||
left_split.operator('pose.rigify_limb_toggle_pole_{rig_id}',
|
left_split.operator('pose.rigify_limb_toggle_pole_{rig_id}',
|
||||||
icon='FORCE_MAGNETIC', properties=op_props)
|
icon='FORCE_MAGNETIC', properties=op_props)
|
||||||
left_split.custom_prop(master, 'pole_vector', text='')
|
text = left_split.expr_if_else(left_split.expr_bone(master)['pole_vector'], 'On', 'Off')
|
||||||
|
left_split.custom_prop(master, 'pole_vector', text=text, toggle=True)
|
||||||
row.operator('pose.rigify_limb_toggle_pole_bake_{rig_id}',
|
row.operator('pose.rigify_limb_toggle_pole_bake_{rig_id}',
|
||||||
text='', icon='ACTION_TWEAK', properties=op_props)
|
text='', icon='ACTION_TWEAK', properties=op_props)
|
||||||
|
51
rigify/ui.py
51
rigify/ui.py
@ -23,7 +23,7 @@ from .utils.widgets import write_widget
|
|||||||
from .utils.naming import unique_name
|
from .utils.naming import unique_name
|
||||||
from .utils.rig import upgrade_metarig_types, outdated_types, upgrade_metarig_layers, \
|
from .utils.rig import upgrade_metarig_types, outdated_types, upgrade_metarig_layers, \
|
||||||
is_valid_metarig, metarig_needs_upgrade
|
is_valid_metarig, metarig_needs_upgrade
|
||||||
from .utils.misc import verify_armature_obj, ArmatureObject, IdPropSequence
|
from .utils.misc import verify_armature_obj, ArmatureObject, IdPropSequence, flatten_children
|
||||||
|
|
||||||
from .rigs.utils import get_limb_generated_names
|
from .rigs.utils import get_limb_generated_names
|
||||||
|
|
||||||
@ -243,6 +243,29 @@ class DATA_PT_rigify_samples(bpy.types.Panel):
|
|||||||
# noinspection SpellCheckingInspection
|
# noinspection SpellCheckingInspection
|
||||||
# noinspection PyPep8Naming
|
# noinspection PyPep8Naming
|
||||||
class DATA_UL_rigify_bone_collections(UIList):
|
class DATA_UL_rigify_bone_collections(UIList):
|
||||||
|
def filter_items(self, _context, data, propname):
|
||||||
|
assert propname == 'collections_all'
|
||||||
|
collections = data.collections_all
|
||||||
|
flags = []
|
||||||
|
|
||||||
|
# Filtering by name
|
||||||
|
if self.filter_name:
|
||||||
|
print(self.filter_name, self.use_filter_invert)
|
||||||
|
flags = bpy.types.UI_UL_list.filter_items_by_name(
|
||||||
|
self.filter_name, self.bitflag_filter_item, collections, "name")
|
||||||
|
if not flags:
|
||||||
|
flags = [self.bitflag_filter_item] * len(collections)
|
||||||
|
|
||||||
|
# Reorder by name.
|
||||||
|
if self.use_filter_sort_alpha:
|
||||||
|
indices = bpy.types.UI_UL_list.sort_items_by_name(collections, "name")
|
||||||
|
# Sort by tree order
|
||||||
|
else:
|
||||||
|
index_map = {c.name: i for i, c in enumerate(flatten_children(data.collections))}
|
||||||
|
indices = [index_map[c.name] for c in collections]
|
||||||
|
|
||||||
|
return flags, indices
|
||||||
|
|
||||||
def draw_item(self, _context, layout, armature, bcoll, _icon, _active_data,
|
def draw_item(self, _context, layout, armature, bcoll, _icon, _active_data,
|
||||||
_active_prop_name, _index=0, _flt_flag=0):
|
_active_prop_name, _index=0, _flt_flag=0):
|
||||||
active_bone = armature.edit_bones.active or armature.bones.active
|
active_bone = armature.edit_bones.active or armature.bones.active
|
||||||
@ -290,9 +313,9 @@ class DATA_PT_rigify_collection_list(bpy.types.Panel):
|
|||||||
|
|
||||||
row.template_list(
|
row.template_list(
|
||||||
"DATA_UL_rigify_bone_collections",
|
"DATA_UL_rigify_bone_collections",
|
||||||
"collections",
|
"",
|
||||||
arm,
|
arm,
|
||||||
"collections",
|
"collections_all",
|
||||||
arm.collections,
|
arm.collections,
|
||||||
"active_index",
|
"active_index",
|
||||||
rows=(4 if active_coll else 1),
|
rows=(4 if active_coll else 1),
|
||||||
@ -322,7 +345,7 @@ class DATA_PT_rigify_collection_list(bpy.types.Panel):
|
|||||||
row.active = active_coll.rigify_ui_row > 0 # noqa
|
row.active = active_coll.rigify_ui_row > 0 # noqa
|
||||||
row.prop(active_coll, "rigify_ui_title")
|
row.prop(active_coll, "rigify_ui_title")
|
||||||
|
|
||||||
if ROOT_COLLECTION not in arm.collections:
|
if ROOT_COLLECTION not in arm.collections_all:
|
||||||
layout.label(text=f"The '{ROOT_COLLECTION}' collection will be added upon generation", icon='INFO')
|
layout.label(text=f"The '{ROOT_COLLECTION}' collection will be added upon generation", icon='INFO')
|
||||||
|
|
||||||
|
|
||||||
@ -337,11 +360,11 @@ class DATA_PT_rigify_collection_ui(bpy.types.Panel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return is_valid_metarig(context) and len(verify_armature_obj(context.object).data.collections)
|
return is_valid_metarig(context) and len(verify_armature_obj(context.object).data.collections_all)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def draw_btn_block(arm: Armature, parent: UILayout, bcoll_id: int, loose=False):
|
def draw_btn_block(arm: Armature, parent: UILayout, bcoll_id: int, loose=False):
|
||||||
bcoll = arm.collections[bcoll_id]
|
bcoll = arm.collections_all[bcoll_id]
|
||||||
block = parent.row(align=True)
|
block = parent.row(align=True)
|
||||||
|
|
||||||
if bcoll == arm.collections.active:
|
if bcoll == arm.collections.active:
|
||||||
@ -364,8 +387,10 @@ class DATA_PT_rigify_collection_ui(bpy.types.Panel):
|
|||||||
row_table = defaultdict(list)
|
row_table = defaultdict(list)
|
||||||
has_buttons = False
|
has_buttons = False
|
||||||
|
|
||||||
for i, bcoll in enumerate(arm.collections):
|
index_map = {c.name: i for i, c in enumerate(arm.collections_all)}
|
||||||
row_table[bcoll.rigify_ui_row].append(i)
|
|
||||||
|
for bcoll in flatten_children(arm.collections):
|
||||||
|
row_table[bcoll.rigify_ui_row].append(index_map[bcoll.name])
|
||||||
|
|
||||||
if bcoll.rigify_ui_row > 0:
|
if bcoll.rigify_ui_row > 0:
|
||||||
has_buttons = True
|
has_buttons = True
|
||||||
@ -469,7 +494,7 @@ class DATA_OT_rigify_collection_set_ui_row(bpy.types.Operator):
|
|||||||
obj = verify_armature_obj(context.object)
|
obj = verify_armature_obj(context.object)
|
||||||
if self.select:
|
if self.select:
|
||||||
obj.data.collections.active_index = self.index
|
obj.data.collections.active_index = self.index
|
||||||
obj.data.collections[self.index].rigify_ui_row = self.row
|
obj.data.collections_all[self.index].rigify_ui_row = self.row
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
@ -495,7 +520,7 @@ class DATA_OT_rigify_collection_add_ui_row(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
obj = verify_armature_obj(context.object)
|
obj = verify_armature_obj(context.object)
|
||||||
for coll in obj.data.collections:
|
for coll in obj.data.collections_all:
|
||||||
if coll.rigify_ui_row >= self.row:
|
if coll.rigify_ui_row >= self.row:
|
||||||
coll.rigify_ui_row += (1 if self.add else -1)
|
coll.rigify_ui_row += (1 if self.add else -1)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
@ -716,7 +741,7 @@ class DATA_OT_rigify_color_set_remove(bpy.types.Operator):
|
|||||||
rigify_colors.remove(self.idx)
|
rigify_colors.remove(self.idx)
|
||||||
|
|
||||||
# set layers references to 0
|
# set layers references to 0
|
||||||
for coll in obj.data.collections:
|
for coll in obj.data.collections_all:
|
||||||
idx = coll.rigify_color_set_id
|
idx = coll.rigify_color_set_id
|
||||||
|
|
||||||
if idx == self.idx + 1:
|
if idx == self.idx + 1:
|
||||||
@ -745,7 +770,7 @@ class DATA_OT_rigify_color_set_remove_all(bpy.types.Operator):
|
|||||||
rigify_colors.remove(0)
|
rigify_colors.remove(0)
|
||||||
|
|
||||||
# set layers references to 0
|
# set layers references to 0
|
||||||
for coll in obj.data.collections:
|
for coll in obj.data.collections_all:
|
||||||
coll.rigify_color_set_id = 0
|
coll.rigify_color_set_id = 0
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
@ -1025,7 +1050,7 @@ class Generate(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
metarig = verify_armature_obj(context.object)
|
metarig = verify_armature_obj(context.object)
|
||||||
|
|
||||||
for bcoll in metarig.data.collections:
|
for bcoll in metarig.data.collections_all:
|
||||||
if bcoll.rigify_ui_row > 0 and bcoll.name not in SPECIAL_COLLECTIONS:
|
if bcoll.rigify_ui_row > 0 and bcoll.name not in SPECIAL_COLLECTIONS:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@ -102,7 +102,7 @@ def ensure_collection_uid(bcoll: BoneCollection):
|
|||||||
uid = zlib.adler32(bcoll.name.encode("utf-8")) & max_uid
|
uid = zlib.adler32(bcoll.name.encode("utf-8")) & max_uid
|
||||||
|
|
||||||
# Ensure the uid is unique within the armature
|
# Ensure the uid is unique within the armature
|
||||||
used_ids = set(coll.rigify_uid for coll in bcoll.id_data.collections)
|
used_ids = set(coll.rigify_uid for coll in bcoll.id_data.collections_all)
|
||||||
|
|
||||||
while uid in used_ids:
|
while uid in used_ids:
|
||||||
uid = random.randint(0, max_uid)
|
uid = random.randint(0, max_uid)
|
||||||
@ -126,14 +126,14 @@ def resolve_collection_reference(obj: ArmatureObject, ref: Any, *,
|
|||||||
arm = obj.data
|
arm = obj.data
|
||||||
|
|
||||||
name = ref.get("name", "")
|
name = ref.get("name", "")
|
||||||
name_coll = arm.collections.get(name) if name else None
|
name_coll = arm.collections_all.get(name) if name else None
|
||||||
|
|
||||||
# First try an exact match of both name and uid
|
# First try an exact match of both name and uid
|
||||||
if name_coll and name_coll.rigify_uid == uid:
|
if name_coll and name_coll.rigify_uid == uid:
|
||||||
return name_coll
|
return name_coll
|
||||||
|
|
||||||
# Then try searching by the uid
|
# Then try searching by the uid
|
||||||
for coll in arm.collections:
|
for coll in arm.collections_all:
|
||||||
if coll.rigify_uid == uid:
|
if coll.rigify_uid == uid:
|
||||||
if update:
|
if update:
|
||||||
ref["name"] = coll.name
|
ref["name"] = coll.name
|
||||||
@ -194,7 +194,7 @@ def validate_collection_references(obj: ArmatureObject):
|
|||||||
# Ensure uids are unique
|
# Ensure uids are unique
|
||||||
known_uids = dict()
|
known_uids = dict()
|
||||||
|
|
||||||
for bcoll in obj.data.collections:
|
for bcoll in obj.data.collections_all:
|
||||||
uid = bcoll.rigify_uid
|
uid = bcoll.rigify_uid
|
||||||
if uid < 0:
|
if uid < 0:
|
||||||
continue
|
continue
|
||||||
|
@ -172,6 +172,20 @@ def find_index(sequence, item, default=None):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_children(iterable: typing.Iterable):
|
||||||
|
"""Enumerate the iterator items as well as their children in the tree order."""
|
||||||
|
for item in iterable:
|
||||||
|
yield item
|
||||||
|
yield from flatten_children(item.children)
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_parents(item):
|
||||||
|
"""Enumerate the item and all its parents."""
|
||||||
|
while item:
|
||||||
|
yield item
|
||||||
|
item = item.parent
|
||||||
|
|
||||||
|
|
||||||
##############################################
|
##############################################
|
||||||
# Lazy references
|
# Lazy references
|
||||||
##############################################
|
##############################################
|
||||||
|
@ -16,7 +16,7 @@ from bpy.types import bpy_prop_array, bpy_prop_collection # noqa
|
|||||||
from idprop.types import IDPropertyArray
|
from idprop.types import IDPropertyArray
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
|
||||||
from .misc import ArmatureObject, wrap_list_to_lines, IdPropSequence, find_index
|
from .misc import ArmatureObject, wrap_list_to_lines, IdPropSequence, find_index, flatten_children
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..base_rig import BaseRig
|
from ..base_rig import BaseRig
|
||||||
@ -193,7 +193,7 @@ def upgrade_metarig_layers(metarig: ArmatureObject):
|
|||||||
# Find layer collections
|
# Find layer collections
|
||||||
coll_table = {}
|
coll_table = {}
|
||||||
|
|
||||||
for coll in arm.collections:
|
for coll in arm.collections_all:
|
||||||
if m := re.match(r'^Layer (\d+)', coll.name):
|
if m := re.match(r'^Layer (\d+)', coll.name):
|
||||||
coll_table[int(m[1]) - 1] = coll
|
coll_table[int(m[1]) - 1] = coll
|
||||||
|
|
||||||
@ -229,7 +229,7 @@ def upgrade_metarig_layers(metarig: ArmatureObject):
|
|||||||
coll.name = new_name
|
coll.name = new_name
|
||||||
|
|
||||||
if coll:
|
if coll:
|
||||||
coll_idx = find_index(arm.collections, coll)
|
coll_idx = find_index(arm.collections_all, coll)
|
||||||
arm.collections.move(coll_idx, cur_idx)
|
arm.collections.move(coll_idx, cur_idx)
|
||||||
cur_idx += 1
|
cur_idx += 1
|
||||||
|
|
||||||
@ -249,7 +249,7 @@ def upgrade_metarig_layers(metarig: ArmatureObject):
|
|||||||
root_bcoll = coll_table.get(28)
|
root_bcoll = coll_table.get(28)
|
||||||
|
|
||||||
used_rows = set()
|
used_rows = set()
|
||||||
for bcoll in arm.collections:
|
for bcoll in arm.collections_all:
|
||||||
if bcoll != root_bcoll and bcoll.rigify_ui_row > 0:
|
if bcoll != root_bcoll and bcoll.rigify_ui_row > 0:
|
||||||
used_rows.add(bcoll.rigify_ui_row)
|
used_rows.add(bcoll.rigify_ui_row)
|
||||||
|
|
||||||
@ -258,7 +258,7 @@ def upgrade_metarig_layers(metarig: ArmatureObject):
|
|||||||
if i in used_rows:
|
if i in used_rows:
|
||||||
row_map[i] = len(row_map) + 1
|
row_map[i] = len(row_map) + 1
|
||||||
|
|
||||||
for bcoll in arm.collections:
|
for bcoll in arm.collections_all:
|
||||||
if bcoll == root_bcoll:
|
if bcoll == root_bcoll:
|
||||||
bcoll.rigify_ui_row = len(row_map) + 3
|
bcoll.rigify_ui_row = len(row_map) + 3
|
||||||
elif bcoll.rigify_ui_row > 0:
|
elif bcoll.rigify_ui_row > 0:
|
||||||
@ -535,13 +535,13 @@ def write_metarig(obj: ArmatureObject, layers=False, func_name="create",
|
|||||||
|
|
||||||
code.append('\n bone_collections = {}')
|
code.append('\n bone_collections = {}')
|
||||||
|
|
||||||
code.append('\n for bcoll in list(arm.collections):'
|
code.append('\n for bcoll in list(arm.collections_all):'
|
||||||
'\n arm.collections.remove(bcoll)\n')
|
'\n arm.collections.remove(bcoll)\n')
|
||||||
|
|
||||||
args = ', '.join(f'{k}={repr(v)}' for k, v in collection_attrs.items())
|
args = ', '.join(f'{k}={repr(v)}' for k, v in collection_attrs.items())
|
||||||
|
|
||||||
code.append(f" def add_bone_collection(name, *, {args}):")
|
code.append(f" def add_bone_collection(name, *, parent=None, {args}):")
|
||||||
code.append(f" new_bcoll = arm.collections.new(name)")
|
code.append(f" new_bcoll = arm.collections.new(name, parent=bone_collections.get(parent))")
|
||||||
for k, _v in collection_attrs.items():
|
for k, _v in collection_attrs.items():
|
||||||
code.append(f" new_bcoll.rigify_{k} = {k}")
|
code.append(f" new_bcoll.rigify_{k} = {k}")
|
||||||
code.append(" bone_collections[name] = new_bcoll")
|
code.append(" bone_collections[name] = new_bcoll")
|
||||||
@ -559,8 +559,10 @@ def write_metarig(obj: ArmatureObject, layers=False, func_name="create",
|
|||||||
ref_list.add().set_collection(bone_collections[name])
|
ref_list.add().set_collection(bone_collections[name])
|
||||||
""")
|
""")
|
||||||
|
|
||||||
for i, bcoll in enumerate(arm.collections):
|
for bcoll in flatten_children(arm.collections):
|
||||||
args = [repr(bcoll.name)]
|
args = [repr(bcoll.name)]
|
||||||
|
if bcoll.parent:
|
||||||
|
args.append(f"parent={bcoll.parent.name!r}")
|
||||||
for k, v in collection_attrs.items():
|
for k, v in collection_attrs.items():
|
||||||
value = getattr(bcoll, "rigify_" + k)
|
value = getattr(bcoll, "rigify_" + k)
|
||||||
if value != v:
|
if value != v:
|
||||||
|
@ -371,14 +371,15 @@ class SwitchParentBuilder(GeneratorPlugin, MechanismUtilityMixin):
|
|||||||
|
|
||||||
parent_names = [parent[1] or strip_prefix(parent[0])
|
parent_names = [parent[1] or strip_prefix(parent[0])
|
||||||
for parent in [('None', 'None'), *parent_bones]]
|
for parent in [('None', 'None'), *parent_bones]]
|
||||||
parent_str = ', '.join(['%s (%d)' % (name, i) for i, name in enumerate(parent_names)])
|
parent_items = [(f"P{i}", name, "") for i, name in enumerate(parent_names)]
|
||||||
|
|
||||||
ctrl_bone = child['ctrl_bone'] or bone
|
ctrl_bone = child['ctrl_bone'] or bone
|
||||||
|
|
||||||
self.make_property(
|
self.make_property(
|
||||||
prop_bone, prop_id, select_index,
|
prop_bone, prop_id, select_index,
|
||||||
min=0, max=len(parent_bones),
|
min=0, max=len(parent_bones),
|
||||||
description='Switch parent of %s: %s' % (ctrl_bone, parent_str)
|
description=f"Switch parent of {ctrl_bone}",
|
||||||
|
items=parent_items,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find which channels don't depend on the parent
|
# Find which channels don't depend on the parent
|
||||||
@ -406,7 +407,7 @@ class SwitchParentBuilder(GeneratorPlugin, MechanismUtilityMixin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
row = panel.row(align=True)
|
row = panel.row(align=True)
|
||||||
left_split = row.split(factor=0.75, align=True)
|
left_split = row.split(factor=0.65, align=True)
|
||||||
left_split.operator('pose.rigify_switch_parent_{rig_id}', text=prop_name,
|
left_split.operator('pose.rigify_switch_parent_{rig_id}', text=prop_name,
|
||||||
icon='DOWNARROW_HLT', properties=op_props)
|
icon='DOWNARROW_HLT', properties=op_props)
|
||||||
left_split.custom_prop(prop_bone, prop_id, text='')
|
left_split.custom_prop(prop_bone, prop_id, text='')
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
translations_tuple = (
|
translations_tuple = (
|
||||||
(("*", ""),
|
(("*", ""),
|
||||||
((), ()),
|
((), ()),
|
||||||
("fr_FR", "Project-Id-Version: Sun Position 3.3.3 (0)\n",
|
("fr_FR", "Project-Id-Version: Sun Position 3.5.4 (0)\n",
|
||||||
(False,
|
(False,
|
||||||
("Blender's translation file (po format).",
|
("Blender's translation file (po format).",
|
||||||
"Copyright (C) 2022 The Blender Foundation.",
|
"Copyright (C) 2022 The Blender Foundation.",
|
||||||
@ -31,12 +31,6 @@ translations_tuple = (
|
|||||||
("fr_FR", "Afficher les infos d’azimut et de hauteur du Soleil",
|
("fr_FR", "Afficher les infos d’azimut et de hauteur du Soleil",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("*", "Daylight Savings"),
|
|
||||||
(("bpy.types.SunPosProperties.use_daylight_savings",),
|
|
||||||
()),
|
|
||||||
("fr_FR", "Heure d’été",
|
|
||||||
(False, ())),
|
|
||||||
),
|
|
||||||
(("*", "Display overlays in the viewport: the direction of the north, analemmas and the Sun surface"),
|
(("*", "Display overlays in the viewport: the direction of the north, analemmas and the Sun surface"),
|
||||||
(("bpy.types.SunPosAddonPreferences.show_overlays",),
|
(("bpy.types.SunPosAddonPreferences.show_overlays",),
|
||||||
()),
|
()),
|
||||||
@ -82,10 +76,10 @@ translations_tuple = (
|
|||||||
("fr_FR", "Options de position du Soleil",
|
("fr_FR", "Options de position du Soleil",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("*", "Sun Position Presets"),
|
(("Operator", "Add Sun Position preset"),
|
||||||
(("bpy.types.SUNPOS_PT_Presets",),
|
(("bpy.types.WORLD_OT_sunpos_add_preset",),
|
||||||
()),
|
()),
|
||||||
("fr_FR", "Préréglages de position du Soleil",
|
("fr_FR", "Ajouter un préréglage de position du Soleil",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("Operator", "Pick Sun in Viewport"),
|
(("Operator", "Pick Sun in Viewport"),
|
||||||
@ -100,6 +94,18 @@ translations_tuple = (
|
|||||||
("fr_FR", "Sélectionner la position du Soleil dans n’importe quelle vue 3D, puis la synchroniser avec l’environnement",
|
("fr_FR", "Sélectionner la position du Soleil dans n’importe quelle vue 3D, puis la synchroniser avec l’environnement",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
|
(("*", "Add a new preset for Sun Position settings"),
|
||||||
|
(("bpy.types.WORLD_OT_sunpos_add_preset",),
|
||||||
|
()),
|
||||||
|
("fr_FR", "Ajouter un nouveau préréglage pour les réglages de position du Soleil",
|
||||||
|
(False, ())),
|
||||||
|
),
|
||||||
|
(("*", "Sun Position Presets"),
|
||||||
|
(("bpy.types.SUNPOS_PT_Presets",),
|
||||||
|
()),
|
||||||
|
("fr_FR", "Préréglages de position du Soleil",
|
||||||
|
(False, ())),
|
||||||
|
),
|
||||||
(("*", "UTC Zone"),
|
(("*", "UTC Zone"),
|
||||||
(("bpy.types.SunPosProperties.UTC_zone",),
|
(("bpy.types.SunPosProperties.UTC_zone",),
|
||||||
()),
|
()),
|
||||||
@ -392,6 +398,12 @@ translations_tuple = (
|
|||||||
("fr_FR", "Utiliser une seule valeur pour le jour de l’année",
|
("fr_FR", "Utiliser une seule valeur pour le jour de l’année",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
|
(("*", "Daylight Savings"),
|
||||||
|
(("bpy.types.SunPosProperties.use_daylight_savings",),
|
||||||
|
()),
|
||||||
|
("fr_FR", "Heure d’été",
|
||||||
|
(False, ())),
|
||||||
|
),
|
||||||
(("*", "Daylight savings time adds 1 hour to standard time"),
|
(("*", "Daylight savings time adds 1 hour to standard time"),
|
||||||
(("bpy.types.SunPosProperties.use_daylight_savings",),
|
(("bpy.types.SunPosProperties.use_daylight_savings",),
|
||||||
()),
|
()),
|
||||||
@ -416,43 +428,55 @@ translations_tuple = (
|
|||||||
("fr_FR", "Année",
|
("fr_FR", "Année",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("*", "Unknown projection"),
|
(("*", "Please select a Sun object"),
|
||||||
(("scripts/addons/sun_position/hdr.py:181",),
|
(("scripts/addons/sun_position/hdr.py:82",),
|
||||||
()),
|
()),
|
||||||
("fr_FR", "Projection inconnue",
|
("fr_FR", "Veuillez sélectionner un objet Soleil",
|
||||||
(False, ())),
|
|
||||||
),
|
|
||||||
(("*", "Enter/LMB: confirm, Esc/RMB: cancel, MMB: pan, mouse wheel: zoom, Ctrl + mouse wheel: set exposure"),
|
|
||||||
(("scripts/addons/sun_position/hdr.py:252",),
|
|
||||||
()),
|
|
||||||
("fr_FR", "Entrée/ClicG : Confirmer, Échap/ClicD : Annuler, ClicM : défiler, "
|
|
||||||
"molette : zoom, Ctrl + molette : exposition",
|
|
||||||
(False, ())),
|
|
||||||
),
|
|
||||||
(("*", "Could not find 3D View"),
|
|
||||||
(("scripts/addons/sun_position/hdr.py:263",),
|
|
||||||
()),
|
|
||||||
("fr_FR", "Impossible de trouver la vue 3D",
|
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("*", "Please select an Environment Texture node"),
|
(("*", "Please select an Environment Texture node"),
|
||||||
(("scripts/addons/sun_position/hdr.py:269",),
|
(("scripts/addons/sun_position/hdr.py:85",
|
||||||
|
"scripts/addons/sun_position/hdr.py:253"),
|
||||||
()),
|
()),
|
||||||
("fr_FR", "Veuillez utiliser un nœud de texture d’environnement",
|
("fr_FR", "Veuillez utiliser un nœud de texture d’environnement",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("*", "Show options and info:"),
|
(("*", "Please select a valid Environment Texture node"),
|
||||||
(("scripts/addons/sun_position/properties.py:297",),
|
(("scripts/addons/sun_position/hdr.py:91",),
|
||||||
()),
|
()),
|
||||||
("fr_FR", "Afficher les options et infos :",
|
("fr_FR", "Veuillez utiliser un nœud de texture d’environnement valide",
|
||||||
|
(False, ())),
|
||||||
|
),
|
||||||
|
(("*", "Could not find 3D View"),
|
||||||
|
(("scripts/addons/sun_position/hdr.py:247",),
|
||||||
|
()),
|
||||||
|
("fr_FR", "Impossible de trouver la vue 3D",
|
||||||
|
(False, ())),
|
||||||
|
),
|
||||||
|
(("*", "Enter/LMB: confirm, Esc/RMB: cancel, MMB: pan, mouse wheel: zoom, Ctrl + mouse wheel: set exposure"),
|
||||||
|
(("scripts/addons/sun_position/hdr.py:264",),
|
||||||
|
()),
|
||||||
|
("fr_FR", "Entrée/ClicG : Confirmer, Échap/ClicD : Annuler, ClicM : défiler, molette : zoom, Ctrl + molette : exposition",
|
||||||
|
(False, ())),
|
||||||
|
),
|
||||||
|
(("*", "Unknown projection"),
|
||||||
|
(("scripts/addons/sun_position/hdr.py:165",),
|
||||||
|
()),
|
||||||
|
("fr_FR", "Projection inconnue",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("*", "ERROR: Could not parse coordinates"),
|
(("*", "ERROR: Could not parse coordinates"),
|
||||||
(("scripts/addons/sun_position/sun_calc.py:54",),
|
(("scripts/addons/sun_position/properties.py:36",),
|
||||||
()),
|
()),
|
||||||
("fr_FR", "ERREUR : Impossible d’analyser les coordonnées",
|
("fr_FR", "ERREUR : Impossible d’analyser les coordonnées",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
|
(("*", "Show options and info:"),
|
||||||
|
(("scripts/addons/sun_position/properties.py:317",),
|
||||||
|
()),
|
||||||
|
("fr_FR", "Afficher les options et infos :",
|
||||||
|
(False, ())),
|
||||||
|
),
|
||||||
(("Hour", "Time"),
|
(("Hour", "Time"),
|
||||||
(("scripts/addons/sun_position/ui_sun.py:224",),
|
(("scripts/addons/sun_position/ui_sun.py:224",),
|
||||||
()),
|
()),
|
||||||
@ -460,13 +484,13 @@ translations_tuple = (
|
|||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("*", "Time Local:"),
|
(("*", "Time Local:"),
|
||||||
(("scripts/addons/sun_position/ui_sun.py:242",),
|
(("scripts/addons/sun_position/ui_sun.py:241",),
|
||||||
()),
|
()),
|
||||||
("fr_FR", "Heure locale :",
|
("fr_FR", "Heure locale :",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
),
|
),
|
||||||
(("*", "UTC:"),
|
(("*", "UTC:"),
|
||||||
(("scripts/addons/sun_position/ui_sun.py:243",),
|
(("scripts/addons/sun_position/ui_sun.py:242",),
|
||||||
()),
|
()),
|
||||||
("fr_FR", "UTC :",
|
("fr_FR", "UTC :",
|
||||||
(False, ())),
|
(False, ())),
|
||||||
|
@ -23,7 +23,7 @@ class SUNPOS_PT_Presets(PresetPanel, bpy.types.Panel):
|
|||||||
|
|
||||||
|
|
||||||
class SUNPOS_OT_AddPreset(AddPresetBase, Operator):
|
class SUNPOS_OT_AddPreset(AddPresetBase, Operator):
|
||||||
'''Add Sun Position preset'''
|
'''Add a new preset for Sun Position settings'''
|
||||||
bl_idname = "world.sunpos_add_preset"
|
bl_idname = "world.sunpos_add_preset"
|
||||||
bl_label = "Add Sun Position preset"
|
bl_label = "Add Sun Position preset"
|
||||||
preset_menu = "SUNPOS_PT_Presets"
|
preset_menu = "SUNPOS_PT_Presets"
|
||||||
|
Loading…
Reference in New Issue
Block a user