Node Wrangler: Improved accuracy on Align Nodes operator #104551

Open
quackarooni wants to merge 18 commits from quackarooni/blender-addons:nw_rework_align_nodes into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
61 changed files with 4471 additions and 1023 deletions
Showing only changes of commit edfb41c14c - Show all commits

View File

@ -29,13 +29,6 @@ translations_tuple = (
("fr_FR", "Choisir un nom pour la catégorie du panneau", ("fr_FR", "Choisir un nom pour la catégorie du panneau",
(False, ())), (False, ())),
), ),
(("Operator", "Insert Key"),
(("bpy.types.ANIM_OT_insert_keyframe_animall",
"bpy.types.ANIM_OT_insert_keyframe_animall"),
()),
("fr_FR", "Insérer une clé",
(False, ())),
),
(("Operator", "Clear Animation"), (("Operator", "Clear Animation"),
(("bpy.types.ANIM_OT_clear_animation_animall",), (("bpy.types.ANIM_OT_clear_animation_animall",),
()), ()),
@ -49,6 +42,12 @@ translations_tuple = (
"En cas déchec, essayez de les supprimer manuellement", "En cas déchec, essayez de les supprimer manuellement",
(False, ())), (False, ())),
), ),
(("Operator", "Insert Key"),
(("bpy.types.ANIM_OT_insert_keyframe_animall",),
()),
("fr_FR", "Insérer une clé",
(False, ())),
),
(("*", "Insert a Keyframe"), (("*", "Insert a Keyframe"),
(("bpy.types.ANIM_OT_insert_keyframe_animall",), (("bpy.types.ANIM_OT_insert_keyframe_animall",),
()), ()),
@ -67,6 +66,18 @@ translations_tuple = (
("fr_FR", "Supprimer une image clé", ("fr_FR", "Supprimer une image clé",
(False, ())), (False, ())),
), ),
(("Operator", "Update Vertex Color Animation"),
(("bpy.types.ANIM_OT_update_vertex_color_animation_animall",),
()),
("fr_FR", "Mettre à jour lanimation des couleurs de sommets",
(False, ())),
),
(("*", "Update old vertex color channel formats from pre-3.3 versions"),
(("bpy.types.ANIM_OT_update_vertex_color_animation_animall",),
()),
("fr_FR", "Mettre à jour les formats des canaux depuis les versions antérieures à la 3.3",
(False, ())),
),
(("*", "Animate"), (("*", "Animate"),
(("bpy.types.VIEW3D_PT_animall",), (("bpy.types.VIEW3D_PT_animall",),
()), ()),

View File

@ -203,36 +203,36 @@ class BlenderIdPreferences(AddonPreferences):
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
if expiry is None: if expiry is None:
layout.label(text='We do not know when your token expires, please validate it.') layout.label(text='We do not know when your token expires, please validate it')
elif now >= expiry: elif now >= expiry:
layout.label(text='Your login has expired! Log out and log in again to refresh it.', layout.label(text='Your login has expired! Log out and log in again to refresh it',
icon='ERROR') icon='ERROR')
else: else:
time_left = expiry - now time_left = expiry - now
if time_left.days > 14: if time_left.days > 14:
exp_str = tip_('on {:%Y-%m-%d}').format(expiry) exp_str = tip_('on {:%Y-%m-%d}').format(expiry)
elif time_left.days > 1: elif time_left.days > 1:
exp_str = tip_('in %i days.') % time_left.days exp_str = tip_('in %i days') % time_left.days
elif time_left.seconds >= 7200: elif time_left.seconds >= 7200:
exp_str = tip_('in %i hours.') % round(time_left.seconds / 3600) exp_str = tip_('in %i hours') % round(time_left.seconds / 3600)
elif time_left.seconds >= 120: elif time_left.seconds >= 120:
exp_str = tip_('in %i minutes.') % round(time_left.seconds / 60) exp_str = tip_('in %i minutes') % round(time_left.seconds / 60)
else: else:
exp_str = tip_('within seconds') exp_str = tip_('within seconds')
endpoint = communication.blender_id_endpoint() endpoint = communication.blender_id_endpoint()
if endpoint == communication.BLENDER_ID_ENDPOINT: if endpoint == communication.BLENDER_ID_ENDPOINT:
msg = tip_('You are logged in as %s.') % active_profile.username msg = tip_('You are logged in as %s') % active_profile.username
else: else:
msg = tip_('You are logged in as %s at %s.') % (active_profile.username, endpoint) msg = tip_('You are logged in as %s at %s') % (active_profile.username, endpoint)
col = layout.column(align=True) col = layout.column(align=True)
col.label(text=msg, icon='WORLD_DATA') col.label(text=msg, icon='WORLD_DATA')
if time_left.days < 14: if time_left.days < 14:
col.label(text=tip_('Your token will expire %s. Please log out and log in again ' col.label(text=tip_('Your token will expire %s. Please log out and log in again '
'to refresh it.') % exp_str, icon='PREVIEW_RANGE') 'to refresh it') % exp_str, icon='PREVIEW_RANGE')
else: else:
col.label(text=tip_('Your authentication token expires %s.') % exp_str, col.label(text=tip_('Your authentication token expires %s') % exp_str,
icon='BLANK1') icon='BLANK1')
row = layout.row().split(factor=0.8) row = layout.row().split(factor=0.8)
@ -307,9 +307,9 @@ class BlenderIdValidate(BlenderIdMixin, Operator):
err = validate_token() err = validate_token()
if err is None: if err is None:
addon_prefs.ok_message = tip_('Authentication token is valid.') addon_prefs.ok_message = tip_('Authentication token is valid')
else: else:
addon_prefs.error_message = tip_('%s; you probably want to log out and log in again.') % err addon_prefs.error_message = tip_('%s; you probably want to log out and log in again') % err
BlenderIdProfile.read_json() BlenderIdProfile.read_json()
@ -329,7 +329,7 @@ class BlenderIdLogout(BlenderIdMixin, Operator):
profiles.logout(BlenderIdProfile.user_id) profiles.logout(BlenderIdProfile.user_id)
BlenderIdProfile.read_json() BlenderIdProfile.read_json()
addon_prefs.ok_message = tip_('You have been logged out.') addon_prefs.ok_message = tip_('You have been logged out')
return {'FINISHED'} return {'FINISHED'}

View File

@ -17,6 +17,7 @@ bl_info = {
"category": "Animation", "category": "Animation",
"support": 'OFFICIAL', "support": 'OFFICIAL',
"doc_url": "{BLENDER_MANUAL_URL}/addons/animation/copy_global_transform.html", "doc_url": "{BLENDER_MANUAL_URL}/addons/animation/copy_global_transform.html",
"tracker_url": "https://projects.blender.org/blender/blender-addons/issues",
} }
import ast import ast

View File

@ -13,6 +13,7 @@ bl_info = {
"version": (0, 1), "version": (0, 1),
"blender": (2, 80, 0), "blender": (2, 80, 0),
"description": "Various dependency graph debugging tools", "description": "Various dependency graph debugging tools",
"location": "Properties > View Layer > Dependency Graph",
"warning": "", "warning": "",
"doc_url": "", "doc_url": "",
"tracker_url": "", "tracker_url": "",

View File

@ -98,7 +98,7 @@ def sorted_nodes(bvh_nodes):
def read_bvh(context, file_path, rotate_mode='XYZ', global_scale=1.0): def read_bvh(context, file_path, rotate_mode='XYZ', global_scale=1.0):
# File loading stuff # File loading stuff
# Open the file for importing # Open the file for importing
file = open(file_path, 'rU') file = open(file_path, 'r')
# Separate into a list of lists, each line a list of words. # Separate into a list of lists, each line a list of words.
file_lines = file.readlines() file_lines = file.readlines()

View File

@ -345,13 +345,11 @@ def load_ply_mesh(filepath, ply_name):
if mesh_faces: if mesh_faces:
loops_vert_idx = [] loops_vert_idx = []
faces_loop_start = [] faces_loop_start = []
faces_loop_total = []
lidx = 0 lidx = 0
for f in mesh_faces: for f in mesh_faces:
nbr_vidx = len(f) nbr_vidx = len(f)
loops_vert_idx.extend(f) loops_vert_idx.extend(f)
faces_loop_start.append(lidx) faces_loop_start.append(lidx)
faces_loop_total.append(nbr_vidx)
lidx += nbr_vidx lidx += nbr_vidx
mesh.loops.add(len(loops_vert_idx)) mesh.loops.add(len(loops_vert_idx))
@ -359,7 +357,6 @@ def load_ply_mesh(filepath, ply_name):
mesh.loops.foreach_set("vertex_index", loops_vert_idx) mesh.loops.foreach_set("vertex_index", loops_vert_idx)
mesh.polygons.foreach_set("loop_start", faces_loop_start) mesh.polygons.foreach_set("loop_start", faces_loop_start)
mesh.polygons.foreach_set("loop_total", faces_loop_total)
if uvindices: if uvindices:
uv_layer = mesh.uv_layers.new() uv_layer = mesh.uv_layers.new()

View File

@ -3,7 +3,7 @@
bl_info = { bl_info = {
"name": "UV Layout", "name": "UV Layout",
"author": "Campbell Barton, Matt Ebb", "author": "Campbell Barton, Matt Ebb",
"version": (1, 1, 5), "version": (1, 1, 6),
"blender": (3, 0, 0), "blender": (3, 0, 0),
"location": "UV Editor > UV > Export UV Layout", "location": "UV Editor > UV > Export UV Layout",
"description": "Export the UV layout as a 2D graphic", "description": "Export the UV layout as a 2D graphic",

View File

@ -6,6 +6,12 @@ from mathutils import Vector, Matrix
from mathutils.geometry import tessellate_polygon from mathutils.geometry import tessellate_polygon
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
# Use OIIO if available, else Blender for writing the image.
try:
import OpenImageIO as oiio
except ImportError:
oiio = None
def export(filepath, face_data, colors, width, height, opacity): def export(filepath, face_data, colors, width, height, opacity):
offscreen = gpu.types.GPUOffScreen(width, height) offscreen = gpu.types.GPUOffScreen(width, height)
@ -44,6 +50,12 @@ def get_normalize_uvs_matrix():
matrix.col[3][1] = -1 matrix.col[3][1] = -1
matrix[0][0] = 2 matrix[0][0] = 2
matrix[1][1] = 2 matrix[1][1] = 2
# OIIO writes arrays from the left-upper corner.
if oiio:
matrix.col[3][1] *= -1.0
matrix[1][1] *= -1.0
return matrix return matrix
@ -90,6 +102,14 @@ def draw_lines(face_data):
def save_pixels(filepath, pixel_data, width, height): def save_pixels(filepath, pixel_data, width, height):
if oiio:
spec = oiio.ImageSpec(width, height, 4, "uint8")
image = oiio.ImageOutput.create(filepath)
image.open(filepath, spec)
image.write_image(pixel_data)
image.close()
return
image = bpy.data.images.new("temp", width, height, alpha=True) image = bpy.data.images.new("temp", width, height, alpha=True)
image.filepath = filepath image.filepath = filepath
image.pixels = [v / 255 for v in pixel_data] image.pixels = [v / 255 for v in pixel_data]

164
io_scene_3ds/__init__.py Normal file
View File

@ -0,0 +1,164 @@
# SPDX-License-Identifier: GPL-2.0-or-later
from bpy_extras.io_utils import (
ImportHelper,
ExportHelper,
orientation_helper,
axis_conversion,
)
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
StringProperty,
)
import bpy
bl_info = {
"name": "Autodesk 3DS format",
"author": "Bob Holcomb, Campbell Barton, Andreas Atteneder, Sebastian Schrand",
"version": (2, 3, 4),
"blender": (3, 6, 0),
"location": "File > Import-Export",
"description": "3DS Import/Export meshes, UVs, materials, textures, "
"cameras, lamps & animation",
"warning": "Images must be in file folder, "
"filenames are limited to DOS 8.3 format",
"doc_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
"Scripts/Import-Export/Autodesk_3DS",
"category": "Import-Export",
}
if "bpy" in locals():
import importlib
if "import_3ds" in locals():
importlib.reload(import_3ds)
if "export_3ds" in locals():
importlib.reload(export_3ds)
@orientation_helper(axis_forward='Y', axis_up='Z')
class Import3DS(bpy.types.Operator, ImportHelper):
"""Import from 3DS file format (.3ds)"""
bl_idname = "import_scene.autodesk_3ds"
bl_label = 'Import 3DS'
bl_options = {'UNDO'}
filename_ext = ".3ds"
filter_glob: StringProperty(default="*.3ds", options={'HIDDEN'})
constrain_size: FloatProperty(
name="Size Constraint",
description="Scale the model by 10 until it reaches the "
"size constraint (0 to disable)",
min=0.0, max=1000.0,
soft_min=0.0, soft_max=1000.0,
default=10.0,
)
use_image_search: BoolProperty(
name="Image Search",
description="Search subdirectories for any associated images "
"(Warning, may be slow)",
default=True,
)
use_apply_transform: BoolProperty(
name="Apply Transform",
description="Workaround for object transformations "
"importing incorrectly",
default=True,
)
read_keyframe: bpy.props.BoolProperty(
name="Read Keyframe",
description="Read the keyframe data",
default=True,
)
use_world_matrix: bpy.props.BoolProperty(
name="World Space",
description="Transform to matrix world",
default=False,
)
def execute(self, context):
from . import import_3ds
keywords = self.as_keywords(ignore=("axis_forward",
"axis_up",
"filter_glob",
))
global_matrix = axis_conversion(from_forward=self.axis_forward,
from_up=self.axis_up,
).to_4x4()
keywords["global_matrix"] = global_matrix
return import_3ds.load(self, context, **keywords)
@orientation_helper(axis_forward='Y', axis_up='Z')
class Export3DS(bpy.types.Operator, ExportHelper):
"""Export to 3DS file format (.3ds)"""
bl_idname = "export_scene.autodesk_3ds"
bl_label = 'Export 3DS'
filename_ext = ".3ds"
filter_glob: StringProperty(
default="*.3ds",
options={'HIDDEN'},
)
use_selection: BoolProperty(
name="Selection Only",
description="Export selected objects only",
default=False,
)
def execute(self, context):
from . import export_3ds
keywords = self.as_keywords(ignore=("axis_forward",
"axis_up",
"filter_glob",
"check_existing",
))
global_matrix = axis_conversion(to_forward=self.axis_forward,
to_up=self.axis_up,
).to_4x4()
keywords["global_matrix"] = global_matrix
return export_3ds.save(self, context, **keywords)
# Add to a menu
def menu_func_export(self, context):
self.layout.operator(Export3DS.bl_idname, text="3D Studio (.3ds)")
def menu_func_import(self, context):
self.layout.operator(Import3DS.bl_idname, text="3D Studio (.3ds)")
def register():
bpy.utils.register_class(Import3DS)
bpy.utils.register_class(Export3DS)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
bpy.utils.unregister_class(Import3DS)
bpy.utils.unregister_class(Export3DS)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
# NOTES:
# why add 1 extra vertex? and remove it when done? -
# "Answer - eekadoodle - would need to re-order UV's without this since face
# order isnt always what we give blender, BMesh will solve :D"
#
# disabled scaling to size, this requires exposing bb (easy) and understanding
# how it works (needs some time)
if __name__ == "__main__":
register()

1437
io_scene_3ds/export_3ds.py Normal file

File diff suppressed because it is too large Load Diff

1338
io_scene_3ds/import_3ds.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
bl_info = { bl_info = {
"name": "FBX format", "name": "FBX format",
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem", "author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
"version": (5, 1, 0), "version": (5, 3, 0),
"blender": (3, 6, 0), "blender": (3, 6, 0),
"location": "File > Import-Export", "location": "File > Import-Export",
"description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions", "description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions",

View File

@ -1431,7 +1431,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
me_fbxmaterials_idx = scene_data.mesh_material_indices.get(me) me_fbxmaterials_idx = scene_data.mesh_material_indices.get(me)
if me_fbxmaterials_idx is not None: if me_fbxmaterials_idx is not None:
# We cannot use me.materials here, as this array is filled with None in case materials are linked to object... # We cannot use me.materials here, as this array is filled with None in case materials are linked to object...
me_blmaterials = [mat_slot.material for mat_slot in me_obj.material_slots] me_blmaterials = me_obj.materials
if me_fbxmaterials_idx and me_blmaterials: if me_fbxmaterials_idx and me_blmaterials:
lay_ma = elem_data_single_int32(geom, b"LayerElementMaterial", 0) lay_ma = elem_data_single_int32(geom, b"LayerElementMaterial", 0)
elem_data_single_int32(lay_ma, b"Version", FBX_GEOMETRY_MATERIAL_VERSION) elem_data_single_int32(lay_ma, b"Version", FBX_GEOMETRY_MATERIAL_VERSION)
@ -2598,6 +2598,14 @@ def fbx_data_from_scene(scene, depsgraph, settings):
bmesh.ops.triangulate(bm, faces=bm.faces) bmesh.ops.triangulate(bm, faces=bm.faces)
bm.to_mesh(tmp_me) bm.to_mesh(tmp_me)
bm.free() bm.free()
# Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry Nodes,
# can change the materials.
orig_mats = tuple(slot.material for slot in ob.material_slots)
eval_mats = tuple(slot.material.original if slot.material else None
for slot in ob_to_convert.material_slots)
if orig_mats != eval_mats:
# Override the default behaviour of getting materials from ob_obj.bdata.material_slots.
ob_obj.override_materials = eval_mats
data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True) data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True)
# Change armatures back. # Change armatures back.
for armature, pose_position in backup_pose_positions: for armature, pose_position in backup_pose_positions:
@ -2713,8 +2721,7 @@ def fbx_data_from_scene(scene, depsgraph, settings):
data_materials = {} data_materials = {}
for ob_obj in objects: for ob_obj in objects:
# If obj is not a valid object for materials, wrapper will just return an empty tuple... # If obj is not a valid object for materials, wrapper will just return an empty tuple...
for ma_s in ob_obj.material_slots: for ma in ob_obj.materials:
ma = ma_s.material
if ma is None: if ma is None:
continue # Empty slots! continue # Empty slots!
# Note theoretically, FBX supports any kind of materials, even GLSL shaders etc. # Note theoretically, FBX supports any kind of materials, even GLSL shaders etc.

View File

@ -244,6 +244,11 @@ def array_to_matrix4(arr):
return Matrix(tuple(zip(*[iter(arr)]*4))).transposed() return Matrix(tuple(zip(*[iter(arr)]*4))).transposed()
def parray_as_ndarray(arr):
"""Convert an array.array into an np.ndarray that shares the same memory"""
return np.frombuffer(arr, dtype=arr.typecode)
def similar_values(v1, v2, e=1e-6): def similar_values(v1, v2, e=1e-6):
"""Return True if v1 and v2 are nearly the same.""" """Return True if v1 and v2 are nearly the same."""
if v1 == v2: if v1 == v2:
@ -1169,7 +1174,7 @@ class ObjectWrapper(metaclass=MetaObjectWrapper):
we need to use a key to identify each. we need to use a key to identify each.
""" """
__slots__ = ( __slots__ = (
'name', 'key', 'bdata', 'parented_to_armature', 'name', 'key', 'bdata', 'parented_to_armature', 'override_materials',
'_tag', '_ref', '_dupli_matrix' '_tag', '_ref', '_dupli_matrix'
) )
@ -1224,6 +1229,7 @@ class ObjectWrapper(metaclass=MetaObjectWrapper):
self.bdata = bdata self.bdata = bdata
self._ref = armature self._ref = armature
self.parented_to_armature = False self.parented_to_armature = False
self.override_materials = None
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, self.__class__) and self.key == other.key return isinstance(other, self.__class__) and self.key == other.key
@ -1438,11 +1444,14 @@ class ObjectWrapper(metaclass=MetaObjectWrapper):
return () return ()
bones = property(get_bones) bones = property(get_bones)
def get_material_slots(self): def get_materials(self):
override_materials = self.override_materials
if override_materials is not None:
return override_materials
if self._tag in {'OB', 'DP'}: if self._tag in {'OB', 'DP'}:
return self.bdata.material_slots return tuple(slot.material for slot in self.bdata.material_slots)
return () return ()
material_slots = property(get_material_slots) materials = property(get_materials)
def is_deformed_by_armature(self, arm_obj): def is_deformed_by_armature(self, arm_obj):
if not (self.is_object and self.type == 'MESH'): if not (self.is_object and self.type == 'MESH'):

View File

@ -18,6 +18,9 @@ import bpy
from bpy.app.translations import pgettext_tip as tip_ from bpy.app.translations import pgettext_tip as tip_
from mathutils import Matrix, Euler, Vector from mathutils import Matrix, Euler, Vector
# Also imported in .fbx_utils, so importing here is unlikely to further affect Blender startup time.
import numpy as np
# ----- # -----
# Utils # Utils
from . import parse_fbx, fbx_utils from . import parse_fbx, fbx_utils
@ -34,6 +37,10 @@ from .fbx_utils import (
similar_values, similar_values,
similar_values_iter, similar_values_iter,
FBXImportSettings, FBXImportSettings,
vcos_transformed,
nors_transformed,
parray_as_ndarray,
astype_view_signedness,
) )
# global singleton, assign on execution # global singleton, assign on execution
@ -454,8 +461,9 @@ def add_vgroup_to_objects(vg_indices, vg_weights, vg_name, objects):
vg = obj.vertex_groups.get(vg_name) vg = obj.vertex_groups.get(vg_name)
if vg is None: if vg is None:
vg = obj.vertex_groups.new(name=vg_name) vg = obj.vertex_groups.new(name=vg_name)
vg_add = vg.add
for i, w in zip(vg_indices, vg_weights): for i, w in zip(vg_indices, vg_weights):
vg.add((i,), w, 'REPLACE') vg_add((i,), w, 'REPLACE')
def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_prepost_rot): def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_prepost_rot):
@ -777,87 +785,258 @@ def blen_read_geom_layerinfo(fbx_layer):
) )
def blen_read_geom_array_setattr(generator, blen_data, blen_attr, fbx_data, stride, item_size, descr, xform): def blen_read_geom_validate_blen_data(blen_data, blen_dtype, item_size):
"""Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx).""" """Validate blen_data when it's not a bpy_prop_collection.
max_blen_idx = len(blen_data) - 1 Returns whether blen_data is a bpy_prop_collection"""
max_fbx_idx = len(fbx_data) - 1 blen_data_is_collection = isinstance(blen_data, bpy.types.bpy_prop_collection)
print_error = True if not blen_data_is_collection:
if item_size > 1:
assert(len(blen_data.shape) == 2)
assert(blen_data.shape[1] == item_size)
assert(blen_data.dtype == blen_dtype)
return blen_data_is_collection
def check_skip(blen_idx, fbx_idx):
nonlocal print_error
if fbx_idx < 0: # Negative values mean 'skip'.
return True
if blen_idx > max_blen_idx:
if print_error:
print("ERROR: too much data in this Blender layer, compared to elements in mesh, skipping!")
print_error = False
return True
if fbx_idx + item_size - 1 > max_fbx_idx:
if print_error:
print("ERROR: not enough data in this FBX layer, skipping!")
print_error = False
return True
return False
def blen_read_geom_parse_fbx_data(fbx_data, stride, item_size):
"""Parse fbx_data as an array.array into a 2d np.ndarray that shares the same memory, where each row is a single
item"""
# Technically stride < item_size could be supported, but there's probably not a use case for it since it would
# result in a view of the data with self-overlapping memory.
assert(stride >= item_size)
# View the array.array as an np.ndarray.
fbx_data_np = parray_as_ndarray(fbx_data)
if stride == item_size:
if item_size > 1:
# Need to make sure fbx_data_np has a whole number of items to be able to view item_size elements per row.
items_remainder = len(fbx_data_np) % item_size
if items_remainder:
print("ERROR: not a whole number of items in this FBX layer, skipping the partial item!")
fbx_data_np = fbx_data_np[:-items_remainder]
fbx_data_np = fbx_data_np.reshape(-1, item_size)
else:
# Create a view of fbx_data_np that is only the first item_size elements of each stride. Note that the view will
# not be C-contiguous.
stride_remainder = len(fbx_data_np) % stride
if stride_remainder:
if stride_remainder < item_size:
print("ERROR: not a whole number of items in this FBX layer, skipping the partial item!")
# Not enough in the remainder for a full item, so cut off the partial stride
fbx_data_np = fbx_data_np[:-stride_remainder]
# Reshape to one stride per row and then create a view that includes only the first item_size elements
# of each stride.
fbx_data_np = fbx_data_np.reshape(-1, stride)[:, :item_size]
else:
print("ERROR: not a whole number of strides in this FBX layer! There are a whole number of items, but"
" this could indicate an error!")
# There is not a whole number of strides, but there is a whole number of items.
# This is a pain to deal with because fbx_data_np.reshape(-1, stride) is not possible.
# A view of just the items can be created using stride_tricks.as_strided by specifying the shape and
# strides of the view manually.
# Extreme care must be taken when using stride_tricks.as_strided because improper usage can result in
# a view that gives access to memory outside the array.
from numpy.lib import stride_tricks
# fbx_data_np should always start off as flat and C-contiguous.
assert(fbx_data_np.strides == (fbx_data_np.itemsize,))
num_whole_strides = len(fbx_data_np) // stride
# Plus the one partial stride that is enough elements for a complete item.
num_items = num_whole_strides + 1
shape = (num_items, item_size)
# strides are the number of bytes to step to get to the next element, for each axis.
step_per_item = fbx_data_np.itemsize * stride
step_per_item_element = fbx_data_np.itemsize
strides = (step_per_item, step_per_item_element)
fbx_data_np = stride_tricks.as_strided(fbx_data_np, shape, strides)
else:
# There's a whole number of strides, so first reshape to one stride per row and then create a view that
# includes only the first item_size elements of each stride.
fbx_data_np = fbx_data_np.reshape(-1, stride)[:, :item_size]
return fbx_data_np
def blen_read_geom_check_fbx_data_length(blen_data, fbx_data_np, is_indices=False):
"""Check that there are the same number of items in blen_data and fbx_data_np.
Returns a tuple of two elements:
0: fbx_data_np or, if fbx_data_np contains more items than blen_data, a view of fbx_data_np with the excess
items removed
1: Whether the returned fbx_data_np contains enough items to completely fill blen_data"""
bl_num_items = len(blen_data)
fbx_num_items = len(fbx_data_np)
enough_data = fbx_num_items >= bl_num_items
if not enough_data:
if is_indices:
print("ERROR: not enough indices in this FBX layer, missing data will be left as default!")
else:
print("ERROR: not enough data in this FBX layer, missing data will be left as default!")
elif fbx_num_items > bl_num_items:
if is_indices:
print("ERROR: too many indices in this FBX layer, skipping excess!")
else:
print("ERROR: too much data in this FBX layer, skipping excess!")
fbx_data_np = fbx_data_np[:bl_num_items]
return fbx_data_np, enough_data
def blen_read_geom_xform(fbx_data_np, xform):
"""xform is either None, or a function that takes fbx_data_np as its only positional argument and returns an
np.ndarray with the same total number of elements as fbx_data_np.
It is acceptable for xform to return an array with a different dtype to fbx_data_np.
Returns xform(fbx_data_np) when xform is not None and ensures the result of xform(fbx_data_np) has the same shape as
fbx_data_np before returning it.
When xform is None, fbx_data_np is returned as is."""
if xform is not None: if xform is not None:
if isinstance(blen_data, list): item_size = fbx_data_np.shape[1]
if item_size == 1: fbx_total_data = fbx_data_np.size
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): fbx_data_np = xform(fbx_data_np)
blen_data[blen_idx] = xform(fbx_data[fbx_idx]) # The amount of data should not be changed by xform
assert(fbx_data_np.size == fbx_total_data)
# Ensure fbx_data_np is still item_size elements per row
if len(fbx_data_np.shape) != 2 or fbx_data_np.shape[1] != item_size:
fbx_data_np = fbx_data_np.reshape(-1, item_size)
return fbx_data_np
def blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_data, stride, item_size, descr,
xform):
"""Generic fbx_layer to blen_data foreach setter for Direct layers.
blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size.
fbx_data must be an array.array."""
fbx_data_np = blen_read_geom_parse_fbx_data(fbx_data, stride, item_size)
fbx_data_np, enough_data = blen_read_geom_check_fbx_data_length(blen_data, fbx_data_np)
fbx_data_np = blen_read_geom_xform(fbx_data_np, xform)
blen_data_is_collection = blen_read_geom_validate_blen_data(blen_data, blen_dtype, item_size)
if blen_data_is_collection:
if not enough_data:
blen_total_data = len(blen_data) * item_size
buffer = np.empty(blen_total_data, dtype=blen_dtype)
# It's not clear what values should be used for the missing data, so read the current values into a buffer.
blen_data.foreach_get(blen_attr, buffer)
# Change the buffer shape to one item per row
buffer.shape = (-1, item_size)
# Copy the fbx data into the start of the buffer
buffer[:len(fbx_data_np)] = fbx_data_np
else: else:
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): # Convert the buffer to the Blender C type of blen_attr
blen_data[blen_idx] = xform(fbx_data[fbx_idx:fbx_idx + item_size]) buffer = astype_view_signedness(fbx_data_np, blen_dtype)
# Set blen_attr of blen_data. The buffer must be flat and C-contiguous, which ravel() ensures
blen_data.foreach_set(blen_attr, buffer.ravel())
else: else:
if item_size == 1: assert(blen_data.size % item_size == 0)
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): blen_data = blen_data.view()
setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx])) blen_data.shape = (-1, item_size)
blen_data[:len(fbx_data_np)] = fbx_data_np
def blen_read_geom_array_foreach_set_indexed(blen_data, blen_attr, blen_dtype, fbx_data, fbx_layer_index, stride,
item_size, descr, xform):
"""Generic fbx_layer to blen_data foreach setter for IndexToDirect layers.
blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size.
fbx_data must be an array.array or a 1d np.ndarray."""
fbx_data_np = blen_read_geom_parse_fbx_data(fbx_data, stride, item_size)
fbx_data_np = blen_read_geom_xform(fbx_data_np, xform)
# fbx_layer_index is allowed to be a 1d np.ndarray for use with blen_read_geom_array_foreach_set_looptovert.
if not isinstance(fbx_layer_index, np.ndarray):
fbx_layer_index = parray_as_ndarray(fbx_layer_index)
fbx_layer_index, enough_indices = blen_read_geom_check_fbx_data_length(blen_data, fbx_layer_index, is_indices=True)
blen_data_is_collection = blen_read_geom_validate_blen_data(blen_data, blen_dtype, item_size)
blen_data_items_len = len(blen_data)
blen_data_len = blen_data_items_len * item_size
fbx_num_items = len(fbx_data_np)
# Find all indices that are out of bounds of fbx_data_np.
min_index_inclusive = -fbx_num_items
max_index_inclusive = fbx_num_items - 1
valid_index_mask = np.equal(fbx_layer_index, fbx_layer_index.clip(min_index_inclusive, max_index_inclusive))
indices_invalid = not valid_index_mask.all()
fbx_data_items = fbx_data_np.reshape(-1, item_size)
if indices_invalid or not enough_indices:
if blen_data_is_collection:
buffer = np.empty(blen_data_len, dtype=blen_dtype)
buffer_item_view = buffer.view()
buffer_item_view.shape = (-1, item_size)
# Since we don't know what the default values should be for the missing data, read the current values into a
# buffer.
blen_data.foreach_get(blen_attr, buffer)
else: else:
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): buffer_item_view = blen_data
setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx:fbx_idx + item_size]))
if not enough_indices:
# Reduce the length of the view to the same length as the number of indices.
buffer_item_view = buffer_item_view[:len(fbx_layer_index)]
# Copy the result of indexing fbx_data_items by each element in fbx_layer_index into the buffer.
if indices_invalid:
print("ERROR: indices in this FBX layer out of bounds of the FBX data, skipping invalid indices!")
buffer_item_view[valid_index_mask] = fbx_data_items[fbx_layer_index[valid_index_mask]]
else: else:
if isinstance(blen_data, list): buffer_item_view[:] = fbx_data_items[fbx_layer_index]
if item_size == 1:
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): if blen_data_is_collection:
blen_data[blen_idx] = fbx_data[fbx_idx] blen_data.foreach_set(blen_attr, buffer.ravel())
else: else:
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): if blen_data_is_collection:
blen_data[blen_idx] = fbx_data[fbx_idx:fbx_idx + item_size] # Cast the buffer to the Blender C type of blen_attr
fbx_data_items = astype_view_signedness(fbx_data_items, blen_dtype)
buffer_items = fbx_data_items[fbx_layer_index]
blen_data.foreach_set(blen_attr, buffer_items.ravel())
else: else:
if item_size == 1: blen_data[:] = fbx_data_items[fbx_layer_index]
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx])
def blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_data, stride, item_size, descr,
xform):
"""Generic fbx_layer to blen_data foreach setter for AllSame layers.
blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size.
fbx_data must be an array.array."""
fbx_data_np = blen_read_geom_parse_fbx_data(fbx_data, stride, item_size)
fbx_data_np = blen_read_geom_xform(fbx_data_np, xform)
blen_data_is_collection = blen_read_geom_validate_blen_data(blen_data, blen_dtype, item_size)
fbx_items_len = len(fbx_data_np)
blen_items_len = len(blen_data)
if fbx_items_len < 1:
print("ERROR: not enough data in this FBX layer, skipping!")
return
if blen_data_is_collection:
# Create an array filled with the value from fbx_data_np
buffer = np.full((blen_items_len, item_size), fbx_data_np[0], dtype=blen_dtype)
blen_data.foreach_set(blen_attr, buffer.ravel())
else: else:
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx): blen_data[:] = fbx_data_np[0]
setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx:fbx_idx + item_size])
for blen_idx, fbx_idx in generator:
if check_skip(blen_idx, fbx_idx):
continue
_process(blen_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx)
# generic generators. def blen_read_geom_array_foreach_set_looptovert(mesh, blen_data, blen_attr, blen_dtype, fbx_data, stride, item_size,
def blen_read_geom_array_gen_allsame(data_len): descr, xform):
return zip(*(range(data_len), (0,) * data_len)) """Generic fbx_layer to blen_data foreach setter for polyloop ByVertice layers.
blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size.
fbx_data must be an array.array"""
def blen_read_geom_array_gen_direct(fbx_data, stride): # The fbx_data is mapped to vertices. To expand fbx_data to polygon loops, get an array of the vertex index of each
fbx_data_len = len(fbx_data) # polygon loop that will then be used to index fbx_data
return zip(*(range(fbx_data_len // stride), range(0, fbx_data_len, stride))) loop_vertex_indices = np.empty(len(mesh.loops), dtype=np.uintc)
mesh.loops.foreach_get("vertex_index", loop_vertex_indices)
blen_read_geom_array_foreach_set_indexed(blen_data, blen_attr, blen_dtype, fbx_data, loop_vertex_indices, stride,
def blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride): item_size, descr, xform)
return ((bi, fi * stride) for bi, fi in enumerate(fbx_layer_index))
def blen_read_geom_array_gen_direct_looptovert(mesh, fbx_data, stride):
fbx_data_len = len(fbx_data) // stride
loops = mesh.loops
for p in mesh.polygons:
for lidx in p.loop_indices:
vidx = loops[lidx].vertex_index
if vidx < fbx_data_len:
yield lidx, vidx * stride
# generic error printers. # generic error printers.
@ -872,7 +1051,7 @@ def blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet=False):
def blen_read_geom_array_mapped_vert( def blen_read_geom_array_mapped_vert(
mesh, blen_data, blen_attr, mesh, blen_data, blen_attr, blen_dtype,
fbx_layer_data, fbx_layer_index, fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
stride, item_size, descr, stride, item_size, descr,
@ -881,15 +1060,15 @@ def blen_read_geom_array_mapped_vert(
if fbx_layer_mapping == b'ByVertice': if fbx_layer_mapping == b'ByVertice':
if fbx_layer_ref == b'Direct': if fbx_layer_ref == b'Direct':
assert(fbx_layer_index is None) assert(fbx_layer_index is None)
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, item_size,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) descr, xform)
return True return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
elif fbx_layer_mapping == b'AllSame': elif fbx_layer_mapping == b'AllSame':
if fbx_layer_ref == b'IndexToDirect': if fbx_layer_ref == b'IndexToDirect':
assert(fbx_layer_index is None) assert(fbx_layer_index is None)
blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)), blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) item_size, descr, xform)
return True return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
else: else:
@ -899,7 +1078,7 @@ def blen_read_geom_array_mapped_vert(
def blen_read_geom_array_mapped_edge( def blen_read_geom_array_mapped_edge(
mesh, blen_data, blen_attr, mesh, blen_data, blen_attr, blen_dtype,
fbx_layer_data, fbx_layer_index, fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
stride, item_size, descr, stride, item_size, descr,
@ -907,15 +1086,15 @@ def blen_read_geom_array_mapped_edge(
): ):
if fbx_layer_mapping == b'ByEdge': if fbx_layer_mapping == b'ByEdge':
if fbx_layer_ref == b'Direct': if fbx_layer_ref == b'Direct':
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, item_size,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) descr, xform)
return True return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
elif fbx_layer_mapping == b'AllSame': elif fbx_layer_mapping == b'AllSame':
if fbx_layer_ref == b'IndexToDirect': if fbx_layer_ref == b'IndexToDirect':
assert(fbx_layer_index is None) assert(fbx_layer_index is None)
blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)), blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) item_size, descr, xform)
return True return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
else: else:
@ -925,7 +1104,7 @@ def blen_read_geom_array_mapped_edge(
def blen_read_geom_array_mapped_polygon( def blen_read_geom_array_mapped_polygon(
mesh, blen_data, blen_attr, mesh, blen_data, blen_attr, blen_dtype,
fbx_layer_data, fbx_layer_index, fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
stride, item_size, descr, stride, item_size, descr,
@ -937,22 +1116,22 @@ def blen_read_geom_array_mapped_polygon(
# We fallback to 'Direct' mapping in this case. # We fallback to 'Direct' mapping in this case.
#~ assert(fbx_layer_index is not None) #~ assert(fbx_layer_index is not None)
if fbx_layer_index is None: if fbx_layer_index is None:
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) item_size, descr, xform)
else: else:
blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride), blen_read_geom_array_foreach_set_indexed(blen_data, blen_attr, blen_dtype, fbx_layer_data,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) fbx_layer_index, stride, item_size, descr, xform)
return True return True
elif fbx_layer_ref == b'Direct': elif fbx_layer_ref == b'Direct':
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, item_size,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) descr, xform)
return True return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
elif fbx_layer_mapping == b'AllSame': elif fbx_layer_mapping == b'AllSame':
if fbx_layer_ref == b'IndexToDirect': if fbx_layer_ref == b'IndexToDirect':
assert(fbx_layer_index is None) assert(fbx_layer_index is None)
blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)), blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) item_size, descr, xform)
return True return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
else: else:
@ -962,7 +1141,7 @@ def blen_read_geom_array_mapped_polygon(
def blen_read_geom_array_mapped_polyloop( def blen_read_geom_array_mapped_polyloop(
mesh, blen_data, blen_attr, mesh, blen_data, blen_attr, blen_dtype,
fbx_layer_data, fbx_layer_index, fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
stride, item_size, descr, stride, item_size, descr,
@ -974,29 +1153,29 @@ def blen_read_geom_array_mapped_polyloop(
# We fallback to 'Direct' mapping in this case. # We fallback to 'Direct' mapping in this case.
#~ assert(fbx_layer_index is not None) #~ assert(fbx_layer_index is not None)
if fbx_layer_index is None: if fbx_layer_index is None:
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) item_size, descr, xform)
else: else:
blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride), blen_read_geom_array_foreach_set_indexed(blen_data, blen_attr, blen_dtype, fbx_layer_data,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) fbx_layer_index, stride, item_size, descr, xform)
return True return True
elif fbx_layer_ref == b'Direct': elif fbx_layer_ref == b'Direct':
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride), blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride, item_size,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) descr, xform)
return True return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
elif fbx_layer_mapping == b'ByVertice': elif fbx_layer_mapping == b'ByVertice':
if fbx_layer_ref == b'Direct': if fbx_layer_ref == b'Direct':
assert(fbx_layer_index is None) assert(fbx_layer_index is None)
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh, fbx_layer_data, stride), blen_read_geom_array_foreach_set_looptovert(mesh, blen_data, blen_attr, blen_dtype, fbx_layer_data, stride,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) item_size, descr, xform)
return True return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
elif fbx_layer_mapping == b'AllSame': elif fbx_layer_mapping == b'AllSame':
if fbx_layer_ref == b'IndexToDirect': if fbx_layer_ref == b'IndexToDirect':
assert(fbx_layer_index is None) assert(fbx_layer_index is None)
blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)), blen_read_geom_array_foreach_set_allsame(blen_data, blen_attr, blen_dtype, fbx_layer_data, stride,
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform) item_size, descr, xform)
return True return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet) blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
else: else:
@ -1021,7 +1200,7 @@ def blen_read_geom_layer_material(fbx_obj, mesh):
blen_data = mesh.polygons blen_data = mesh.polygons
blen_read_geom_array_mapped_polygon( blen_read_geom_array_mapped_polygon(
mesh, blen_data, "material_index", mesh, blen_data, "material_index", np.uintc,
fbx_layer_data, None, fbx_layer_data, None,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
1, 1, layer_id, 1, 1, layer_id,
@ -1055,7 +1234,7 @@ def blen_read_geom_layer_uv(fbx_obj, mesh):
continue continue
blen_read_geom_array_mapped_polyloop( blen_read_geom_array_mapped_polyloop(
mesh, blen_data, "uv", mesh, blen_data, "uv", np.single,
fbx_layer_data, fbx_layer_index, fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
2, 2, layer_id, 2, 2, layer_id,
@ -1095,7 +1274,7 @@ def blen_read_geom_layer_color(fbx_obj, mesh, colors_type):
continue continue
blen_read_geom_array_mapped_polyloop( blen_read_geom_array_mapped_polyloop(
mesh, blen_data, color_prop_name, mesh, blen_data, color_prop_name, np.single,
fbx_layer_data, fbx_layer_index, fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
4, 4, layer_id, 4, 4, layer_id,
@ -1129,11 +1308,11 @@ def blen_read_geom_layer_smooth(fbx_obj, mesh):
blen_data = mesh.edges blen_data = mesh.edges
blen_read_geom_array_mapped_edge( blen_read_geom_array_mapped_edge(
mesh, blen_data, "use_edge_sharp", mesh, blen_data, "use_edge_sharp", bool,
fbx_layer_data, None, fbx_layer_data, None,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
1, 1, layer_id, 1, 1, layer_id,
xform=lambda s: not s, xform=np.logical_not,
) )
# We only set sharp edges here, not face smoothing itself... # We only set sharp edges here, not face smoothing itself...
mesh.use_auto_smooth = True mesh.use_auto_smooth = True
@ -1141,7 +1320,7 @@ def blen_read_geom_layer_smooth(fbx_obj, mesh):
elif fbx_layer_mapping == b'ByPolygon': elif fbx_layer_mapping == b'ByPolygon':
blen_data = mesh.polygons blen_data = mesh.polygons
return blen_read_geom_array_mapped_polygon( return blen_read_geom_array_mapped_polygon(
mesh, blen_data, "use_smooth", mesh, blen_data, "use_smooth", bool,
fbx_layer_data, None, fbx_layer_data, None,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
1, 1, layer_id, 1, 1, layer_id,
@ -1152,8 +1331,6 @@ def blen_read_geom_layer_smooth(fbx_obj, mesh):
return False return False
def blen_read_geom_layer_edge_crease(fbx_obj, mesh): def blen_read_geom_layer_edge_crease(fbx_obj, mesh):
from math import sqrt
fbx_layer = elem_find_first(fbx_obj, b'LayerElementEdgeCrease') fbx_layer = elem_find_first(fbx_obj, b'LayerElementEdgeCrease')
if fbx_layer is None: if fbx_layer is None:
@ -1184,13 +1361,13 @@ def blen_read_geom_layer_edge_crease(fbx_obj, mesh):
blen_data = mesh.edges blen_data = mesh.edges
return blen_read_geom_array_mapped_edge( return blen_read_geom_array_mapped_edge(
mesh, blen_data, "crease", mesh, blen_data, "crease", np.single,
fbx_layer_data, None, fbx_layer_data, None,
fbx_layer_mapping, fbx_layer_ref, fbx_layer_mapping, fbx_layer_ref,
1, 1, layer_id, 1, 1, layer_id,
# Blender squares those values before sending them to OpenSubdiv, when other software don't, # Blender squares those values before sending them to OpenSubdiv, when other software don't,
# so we need to compensate that to get similar results through FBX... # so we need to compensate that to get similar results through FBX...
xform=sqrt, xform=np.sqrt,
) )
else: else:
print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping)) print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping))
@ -1215,22 +1392,28 @@ def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None):
print("warning %r %r missing data" % (layer_id, fbx_layer_name)) print("warning %r %r missing data" % (layer_id, fbx_layer_name))
return False return False
# try loops, then vertices. # Normals are temporarily set here so that they can be retrieved again after a call to Mesh.validate().
bl_norm_dtype = np.single
item_size = 3
# try loops, then polygons, then vertices.
tries = ((mesh.loops, "Loops", False, blen_read_geom_array_mapped_polyloop), tries = ((mesh.loops, "Loops", False, blen_read_geom_array_mapped_polyloop),
(mesh.polygons, "Polygons", True, blen_read_geom_array_mapped_polygon), (mesh.polygons, "Polygons", True, blen_read_geom_array_mapped_polygon),
(mesh.vertices, "Vertices", True, blen_read_geom_array_mapped_vert)) (mesh.vertices, "Vertices", True, blen_read_geom_array_mapped_vert))
for blen_data, blen_data_type, is_fake, func in tries: for blen_data, blen_data_type, is_fake, func in tries:
bdata = [None] * len(blen_data) if is_fake else blen_data bdata = np.zeros((len(blen_data), item_size), dtype=bl_norm_dtype) if is_fake else blen_data
if func(mesh, bdata, "normal", if func(mesh, bdata, "normal", bl_norm_dtype,
fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 3, 3, layer_id, xform, True): fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 3, item_size, layer_id, xform, True):
if blen_data_type == "Polygons": if blen_data_type == "Polygons":
for pidx, p in enumerate(mesh.polygons): # To expand to per-loop normals, repeat each per-polygon normal by the number of loops of each polygon.
for lidx in range(p.loop_start, p.loop_start + p.loop_total): poly_loop_totals = np.empty(len(mesh.polygons), dtype=np.uintc)
mesh.loops[lidx].normal[:] = bdata[pidx] mesh.polygons.foreach_get("loop_total", poly_loop_totals)
loop_normals = np.repeat(bdata, poly_loop_totals, axis=0)
mesh.loops.foreach_set("normal", loop_normals.ravel())
elif blen_data_type == "Vertices": elif blen_data_type == "Vertices":
# We have to copy vnors to lnors! Far from elegant, but simple. # We have to copy vnors to lnors! Far from elegant, but simple.
for l in mesh.loops: loop_vertex_indices = np.empty(len(mesh.loops), dtype=np.uintc)
l.normal[:] = bdata[l.vertex_index] mesh.loops.foreach_get("vertex_index", loop_vertex_indices)
mesh.loops.foreach_set("normal", bdata[loop_vertex_indices].ravel())
return True return True
blen_read_geom_array_error_mapping("normal", fbx_layer_mapping) blen_read_geom_array_error_mapping("normal", fbx_layer_mapping)
@ -1239,9 +1422,6 @@ def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None):
def blen_read_geom(fbx_tmpl, fbx_obj, settings): def blen_read_geom(fbx_tmpl, fbx_obj, settings):
from itertools import chain
import array
# Vertices are in object space, but we are post-multiplying all transforms with the inverse of the # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
# global matrix, so we need to apply the global matrix to the vertices to get the correct result. # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
geom_mat_co = settings.global_matrix if settings.bake_space_transform else None geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
@ -1259,73 +1439,95 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings):
fbx_polys = elem_prop_first(elem_find_first(fbx_obj, b'PolygonVertexIndex')) fbx_polys = elem_prop_first(elem_find_first(fbx_obj, b'PolygonVertexIndex'))
fbx_edges = elem_prop_first(elem_find_first(fbx_obj, b'Edges')) fbx_edges = elem_prop_first(elem_find_first(fbx_obj, b'Edges'))
if geom_mat_co is not None: bl_vcos_dtype = np.single
def _vcos_transformed_gen(raw_cos, m=None):
# Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
return chain(*(m @ Vector(v) for v in zip(*(iter(raw_cos),) * 3)))
fbx_verts = array.array(fbx_verts.typecode, _vcos_transformed_gen(fbx_verts, geom_mat_co))
if fbx_verts is None: # The dtypes when empty don't matter, but are set to what the fbx arrays are expected to be.
fbx_verts = () fbx_verts = parray_as_ndarray(fbx_verts) if fbx_verts else np.empty(0, dtype=data_types.ARRAY_FLOAT64)
if fbx_polys is None: fbx_polys = parray_as_ndarray(fbx_polys) if fbx_polys else np.empty(0, dtype=data_types.ARRAY_INT32)
fbx_polys = () fbx_edges = parray_as_ndarray(fbx_edges) if fbx_edges else np.empty(0, dtype=data_types.ARRAY_INT32)
# Each vert is a 3d vector so is made of 3 components.
tot_verts = len(fbx_verts) // 3
if tot_verts * 3 != len(fbx_verts):
print("ERROR: Not a whole number of vertices. Ignoring the partial vertex!")
# Remove any remainder.
fbx_verts = fbx_verts[:tot_verts * 3]
tot_loops = len(fbx_polys)
tot_edges = len(fbx_edges)
mesh = bpy.data.meshes.new(name=elem_name_utf8) mesh = bpy.data.meshes.new(name=elem_name_utf8)
mesh.vertices.add(len(fbx_verts) // 3)
mesh.vertices.foreach_set("co", fbx_verts)
if fbx_polys: if tot_verts:
mesh.loops.add(len(fbx_polys)) if geom_mat_co is not None:
poly_loop_starts = [] fbx_verts = vcos_transformed(fbx_verts, geom_mat_co, bl_vcos_dtype)
poly_loop_totals = [] else:
poly_loop_prev = 0 fbx_verts = fbx_verts.astype(bl_vcos_dtype, copy=False)
for i, l in enumerate(mesh.loops):
index = fbx_polys[i]
if index < 0:
poly_loop_starts.append(poly_loop_prev)
poly_loop_totals.append((i - poly_loop_prev) + 1)
poly_loop_prev = i + 1
index ^= -1
l.vertex_index = index
mesh.polygons.add(len(poly_loop_starts)) mesh.vertices.add(tot_verts)
mesh.vertices.foreach_set("co", fbx_verts.ravel())
if tot_loops:
bl_loop_start_dtype = bl_loop_vertex_index_dtype = np.uintc
mesh.loops.add(tot_loops)
# The end of each polygon is specified by an inverted index.
fbx_loop_end_idx = np.flatnonzero(fbx_polys < 0)
tot_polys = len(fbx_loop_end_idx)
# Un-invert the loop ends.
fbx_polys[fbx_loop_end_idx] ^= -1
# Set loop vertex indices, casting to the Blender C type first for performance.
mesh.loops.foreach_set("vertex_index", astype_view_signedness(fbx_polys, bl_loop_vertex_index_dtype))
poly_loop_starts = np.empty(tot_polys, dtype=bl_loop_start_dtype)
# The first loop is always a loop start.
poly_loop_starts[0] = 0
# Ignoring the last loop end, the indices after every loop end are the remaining loop starts.
poly_loop_starts[1:] = fbx_loop_end_idx[:-1] + 1
mesh.polygons.add(tot_polys)
mesh.polygons.foreach_set("loop_start", poly_loop_starts) mesh.polygons.foreach_set("loop_start", poly_loop_starts)
mesh.polygons.foreach_set("loop_total", poly_loop_totals)
blen_read_geom_layer_material(fbx_obj, mesh) blen_read_geom_layer_material(fbx_obj, mesh)
blen_read_geom_layer_uv(fbx_obj, mesh) blen_read_geom_layer_uv(fbx_obj, mesh)
blen_read_geom_layer_color(fbx_obj, mesh, settings.colors_type) blen_read_geom_layer_color(fbx_obj, mesh, settings.colors_type)
if fbx_edges: if tot_edges:
# edges in fact index the polygons (NOT the vertices) # edges in fact index the polygons (NOT the vertices)
import array bl_edge_vertex_indices_dtype = np.uintc
tot_edges = len(fbx_edges)
edges_conv = array.array('i', [0]) * (tot_edges * 2)
edge_index = 0 # The first vertex index of each edge is the vertex index of the corresponding loop in fbx_polys.
for i in fbx_edges: edges_a = fbx_polys[fbx_edges]
e_a = fbx_polys[i]
if e_a >= 0:
e_b = fbx_polys[i + 1]
if e_b < 0:
e_b ^= -1
else:
# Last index of polygon, wrap back to the start.
# ideally we wouldn't have to search back, # The second vertex index of each edge is the vertex index of the next loop in the same polygon. The
# but it should only be 2-3 iterations. # complexity here is that if the first vertex index was the last loop of that polygon in fbx_polys, the next
j = i - 1 # loop in the polygon is the first loop of that polygon, which is not the next loop in fbx_polys.
while j >= 0 and fbx_polys[j] >= 0:
j -= 1
e_a ^= -1
e_b = fbx_polys[j + 1]
edges_conv[edge_index] = e_a # Copy fbx_polys, but rolled backwards by 1 so that indexing the result by [fbx_edges] will get the next
edges_conv[edge_index + 1] = e_b # loop of the same polygon unless the first vertex index was the last loop of the polygon.
edge_index += 2 fbx_polys_next = np.roll(fbx_polys, -1)
# Get the first loop of each polygon and set them into fbx_polys_next at the same indices as the last loop
# of each polygon in fbx_polys.
fbx_polys_next[fbx_loop_end_idx] = fbx_polys[poly_loop_starts]
mesh.edges.add(tot_edges) # Indexing fbx_polys_next by fbx_edges now gets the vertex index of the next loop in fbx_polys.
mesh.edges.foreach_set("vertices", edges_conv) edges_b = fbx_polys_next[fbx_edges]
# edges_a and edges_b need to be combined so that the first vertex index of each edge is immediately
# followed by the second vertex index of that same edge.
# Stack edges_a and edges_b as individual columns like np.column_stack((edges_a, edges_b)).
# np.concatenate is used because np.column_stack doesn't allow specifying the dtype of the returned array.
edges_conv = np.concatenate((edges_a.reshape(-1, 1), edges_b.reshape(-1, 1)),
axis=1, dtype=bl_edge_vertex_indices_dtype, casting='unsafe')
# Add the edges and set their vertex indices.
mesh.edges.add(len(edges_conv))
# ravel() because edges_conv must be flat and C-contiguous when passed to foreach_set.
mesh.edges.foreach_set("vertices", edges_conv.ravel())
elif tot_edges:
print("ERROR: No polygons, but edges exist. Ignoring the edges!")
# must be after edge, face loading. # must be after edge, face loading.
ok_smooth = blen_read_geom_layer_smooth(fbx_obj, mesh) ok_smooth = blen_read_geom_layer_smooth(fbx_obj, mesh)
@ -1340,21 +1542,23 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings):
if geom_mat_no is None: if geom_mat_no is None:
ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh) ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh)
else: else:
def nortrans(v): ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh,
return geom_mat_no @ Vector(v) lambda v_array: nors_transformed(v_array, geom_mat_no))
ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh, nortrans)
mesh.validate(clean_customdata=False) # *Very* important to not remove lnors here! mesh.validate(clean_customdata=False) # *Very* important to not remove lnors here!
if ok_normals: if ok_normals:
clnors = array.array('f', [0.0] * (len(mesh.loops) * 3)) bl_nors_dtype = np.single
clnors = np.empty(len(mesh.loops) * 3, dtype=bl_nors_dtype)
mesh.loops.foreach_get("normal", clnors) mesh.loops.foreach_get("normal", clnors)
if not ok_smooth: if not ok_smooth:
mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons)) mesh.polygons.foreach_set("use_smooth", np.full(len(mesh.polygons), True, dtype=bool))
ok_smooth = True ok_smooth = True
mesh.normals_split_custom_set(tuple(zip(*(iter(clnors),) * 3))) # Iterating clnors into a nested tuple first is faster than passing clnors.reshape(-1, 3) directly into
# normals_split_custom_set. We use clnors.data since it is a memoryview, which is faster to iterate than clnors.
mesh.normals_split_custom_set(tuple(zip(*(iter(clnors.data),) * 3)))
mesh.use_auto_smooth = True mesh.use_auto_smooth = True
else: else:
mesh.calc_normals() mesh.calc_normals()
@ -1363,7 +1567,7 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings):
mesh.free_normals_split() mesh.free_normals_split()
if not ok_smooth: if not ok_smooth:
mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons)) mesh.polygons.foreach_set("use_smooth", np.full(len(mesh.polygons), True, dtype=bool))
if settings.use_custom_props: if settings.use_custom_props:
blen_read_custom_properties(fbx_obj, mesh, settings) blen_read_custom_properties(fbx_obj, mesh, settings)
@ -1371,46 +1575,78 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings):
return mesh return mesh
def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene): def blen_read_shapes(fbx_tmpl, fbx_data, objects, me, scene):
if not fbx_data:
# No shape key data. Nothing to do.
return
bl_vcos_dtype = np.single
me_vcos = np.empty(len(me.vertices) * 3, dtype=bl_vcos_dtype)
me.vertices.foreach_get("co", me_vcos)
me_vcos_vector_view = me_vcos.reshape(-1, 3)
objects = list({node.bl_obj for node in objects})
assert(objects)
bc_uuid_to_keyblocks = {}
for bc_uuid, fbx_sdata, fbx_bcdata in fbx_data:
elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry') elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry')
indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'), default=()) indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'))
dvcos = tuple(co for co in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'), default=()))] * 3)) dvcos = elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'))
indices = parray_as_ndarray(indices) if indices else np.empty(0, dtype=data_types.ARRAY_INT32)
dvcos = parray_as_ndarray(dvcos) if dvcos else np.empty(0, dtype=data_types.ARRAY_FLOAT64)
# If there's not a whole number of vectors, trim off the remainder.
# 3 components per vector.
remainder = len(dvcos) % 3
if remainder:
dvcos = dvcos[:-remainder]
dvcos = dvcos.reshape(-1, 3)
# We completely ignore normals here! # We completely ignore normals here!
weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0 weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0
vgweights = tuple(vgw / 100.0 for vgw in elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights'), default=()))
vgweights = elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights'))
vgweights = parray_as_ndarray(vgweights) if vgweights else np.empty(0, dtype=data_types.ARRAY_FLOAT64)
# Not doing the division in-place in-case it's possible for FBX shape keys to be used by more than one mesh.
vgweights = vgweights / 100.0
create_vg = (vgweights != 1.0).any()
# Special case, in case all weights are the same, FullWeight can have only one element - *sigh!* # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
nbr_indices = len(indices) nbr_indices = len(indices)
if len(vgweights) == 1 and nbr_indices > 1: if len(vgweights) == 1 and nbr_indices > 1:
vgweights = (vgweights[0],) * nbr_indices vgweights = np.full_like(indices, vgweights[0], dtype=vgweights.dtype)
assert(len(vgweights) == nbr_indices == len(dvcos)) assert(len(vgweights) == nbr_indices == len(dvcos))
create_vg = bool(set(vgweights) - {1.0})
keyblocks = []
for me, objects in meshes:
vcos = tuple((idx, me.vertices[idx].co + Vector(dvco)) for idx, dvco in zip(indices, dvcos))
objects = list({node.bl_obj for node in objects})
assert(objects)
# To add shape keys to the mesh, an Object using the mesh is needed.
if me.shape_keys is None: if me.shape_keys is None:
objects[0].shape_key_add(name="Basis", from_mix=False) objects[0].shape_key_add(name="Basis", from_mix=False)
kb = objects[0].shape_key_add(name=elem_name_utf8, from_mix=False) kb = objects[0].shape_key_add(name=elem_name_utf8, from_mix=False)
me.shape_keys.use_relative = True # Should already be set as such. me.shape_keys.use_relative = True # Should already be set as such.
for idx, co in vcos: # Only need to set the shape key co if there are any non-zero dvcos.
kb.data[idx].co[:] = co if dvcos.any():
shape_cos = me_vcos_vector_view.copy()
shape_cos[indices] += dvcos
kb.data.foreach_set("co", shape_cos.ravel())
kb.value = weight kb.value = weight
# Add vgroup if necessary. # Add vgroup if necessary.
if create_vg: if create_vg:
vgoups = add_vgroup_to_objects(indices, vgweights, kb.name, objects) # VertexGroup.add only allows sequences of int indices, but iterating the indices array directly would
# produce numpy scalars of types such as np.int32. The underlying memoryview of the indices array, however,
# does produce standard Python ints when iterated, so pass indices.data to add_vgroup_to_objects instead of
# indices.
# memoryviews tend to be faster to iterate than numpy arrays anyway, so vgweights.data is passed too.
add_vgroup_to_objects(indices.data, vgweights.data, kb.name, objects)
kb.vertex_group = kb.name kb.vertex_group = kb.name
keyblocks.append(kb) bc_uuid_to_keyblocks.setdefault(bc_uuid, []).append(kb)
return bc_uuid_to_keyblocks
return keyblocks
# -------- # --------
@ -2861,6 +3097,7 @@ def load(operator, context, filepath="",
def _(): def _():
fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape')) fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape'))
mesh_to_shapes = {}
for s_uuid, s_item in fbx_table_nodes.items(): for s_uuid, s_item in fbx_table_nodes.items():
fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None)) fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None))
if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape': if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape':
@ -2873,8 +3110,6 @@ def load(operator, context, filepath="",
fbx_bcdata, _bl_bcdata = fbx_table_nodes.get(bc_uuid, (None, None)) fbx_bcdata, _bl_bcdata = fbx_table_nodes.get(bc_uuid, (None, None))
if fbx_bcdata is None or fbx_bcdata.id != b'Deformer' or fbx_bcdata.props[2] != b'BlendShapeChannel': if fbx_bcdata is None or fbx_bcdata.id != b'Deformer' or fbx_bcdata.props[2] != b'BlendShapeChannel':
continue continue
meshes = []
objects = []
for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()): for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()):
if bs_ctype.props[0] != b'OO': if bs_ctype.props[0] != b'OO':
continue continue
@ -2889,6 +3124,9 @@ def load(operator, context, filepath="",
continue continue
# Blenmeshes are assumed already created at that time! # Blenmeshes are assumed already created at that time!
assert(isinstance(bl_mdata, bpy.types.Mesh)) assert(isinstance(bl_mdata, bpy.types.Mesh))
# Group shapes by mesh so that each mesh only needs to be processed once for all of its shape
# keys.
if bl_mdata not in mesh_to_shapes:
# And we have to find all objects using this mesh! # And we have to find all objects using this mesh!
objects = [] objects = []
for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()): for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
@ -2897,12 +3135,18 @@ def load(operator, context, filepath="",
node = fbx_helper_nodes[o_uuid] node = fbx_helper_nodes[o_uuid]
if node: if node:
objects.append(node) objects.append(node)
meshes.append((bl_mdata, objects)) shapes_list = []
mesh_to_shapes[bl_mdata] = (objects, shapes_list)
else:
shapes_list = mesh_to_shapes[bl_mdata][1]
shapes_list.append((bc_uuid, fbx_sdata, fbx_bcdata))
# BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do. # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
# Iterate through each mesh and create its shape keys
for bl_mdata, (objects, shapes) in mesh_to_shapes.items():
for bc_uuid, keyblocks in blen_read_shapes(fbx_tmpl, shapes, objects, bl_mdata, scene).items():
# keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation. # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
keyblocks = blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene) blend_shape_channels.setdefault(bc_uuid, []).extend(keyblocks)
blend_shape_channels[bc_uuid] = keyblocks
_(); del _ _(); del _
if settings.use_subsurf: if settings.use_subsurf:
@ -3224,8 +3468,16 @@ def load(operator, context, filepath="",
if decal_offset != 0.0: if decal_offset != 0.0:
for material in mesh.materials: for material in mesh.materials:
if material in material_decals: if material in material_decals:
for v in mesh.vertices: num_verts = len(mesh.vertices)
v.co += v.normal * decal_offset blen_cos_dtype = blen_norm_dtype = np.single
vcos = np.empty(num_verts * 3, dtype=blen_cos_dtype)
vnorm = np.empty(num_verts * 3, dtype=blen_norm_dtype)
mesh.vertices.foreach_get("co", vcos)
mesh.vertices.foreach_get("normal", vnorm)
vcos += vnorm * decal_offset
mesh.vertices.foreach_set("co", vcos)
break break
for obj in (obj for obj in bpy.data.objects if obj.data == mesh): for obj in (obj for obj in bpy.data.objects if obj.data == mesh):

View File

@ -4,7 +4,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": (3, 6, 6), "version": (3, 6, 15),
'blender': (3, 5, 0), 'blender': (3, 5, 0),
'location': 'File > Import-Export', 'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0', 'description': 'Import-Export as glTF 2.0',
@ -106,7 +106,7 @@ def on_export_format_changed(self, context):
class ConvertGLTF2_Base: class ConvertGLTF2_Base:
"""Base class containing options that should be exposed during both import and export.""" """Base class containing options that should be exposed during both import and export."""
convert_lighting_mode: EnumProperty( export_import_convert_lighting_mode: EnumProperty(
name='Lighting Mode', name='Lighting Mode',
items=( items=(
('SPEC', 'Standard', 'Physically-based glTF lighting units (cd, lx, nt)'), ('SPEC', 'Standard', 'Physically-based glTF lighting units (cd, lx, nt)'),
@ -450,7 +450,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_hierarchy_flatten_bones: BoolProperty( export_hierarchy_flatten_bones: BoolProperty(
name='Flatten Bone Hierarchy', name='Flatten Bone Hierarchy',
description='Flatten Bone Hierarchy. Usefull in case of non decomposable TRS matrix', description='Flatten Bone Hierarchy. Useful in case of non decomposable transformation matrix',
default=False default=False
) )
@ -458,16 +458,18 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
name='Optimize Animation Size', name='Optimize Animation Size',
description=( description=(
"Reduce exported file size by removing duplicate keyframes" "Reduce exported file size by removing duplicate keyframes"
"(can cause problems with stepped animation)"
), ),
default=True default=True
) )
export_optimize_animation_keep_anim_armature: BoolProperty( export_optimize_animation_keep_anim_armature: BoolProperty(
name='Force keeping channel for armature / bones', name='Force keeping channels for bones',
description=( description=(
"if all keyframes are identical in a rig " "if all keyframes are identical in a rig, "
"force keeping the minimal animation" "force keeping the minimal animation. "
"When off, all possible channels for "
"the bones will be exported, even if empty "
"(minimal animation, 2 keyframes)"
), ),
default=True default=True
) )
@ -475,7 +477,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_optimize_animation_keep_anim_object: BoolProperty( export_optimize_animation_keep_anim_object: BoolProperty(
name='Force keeping channel for objects', name='Force keeping channel for objects',
description=( description=(
"if all keyframes are identical for object transformations " "If all keyframes are identical for object transformations, "
"force keeping the minimal animation" "force keeping the minimal animation"
), ),
default=False default=False
@ -488,7 +490,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
('CROP', 'Crop', ('CROP', 'Crop',
'Keep only frames above frame 0'), 'Keep only frames above frame 0'),
), ),
description='Negative Frames are slided or cropped', description='Negative Frames are slid or cropped',
default='SLIDE' default='SLIDE'
) )
@ -496,7 +498,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
name='Set all glTF Animation starting at 0', name='Set all glTF Animation starting at 0',
description=( description=(
"Set all glTF animation starting at 0.0s. " "Set all glTF animation starting at 0.0s. "
"Can be usefull for looping animations" "Can be useful for looping animations"
), ),
default=False default=False
) )
@ -505,7 +507,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
name='Bake All Objects Animations', name='Bake All Objects Animations',
description=( description=(
"Force exporting animation on every objects. " "Force exporting animation on every objects. "
"Can be usefull when using constraints or driver. " "Can be useful when using constraints or driver. "
"Also useful when exporting only selection" "Also useful when exporting only selection"
), ),
default=False default=False
@ -786,7 +788,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_settings['gltf_morph_anim'] = False export_settings['gltf_morph_anim'] = False
export_settings['gltf_lights'] = self.export_lights export_settings['gltf_lights'] = self.export_lights
export_settings['gltf_lighting_mode'] = self.convert_lighting_mode export_settings['gltf_lighting_mode'] = self.export_import_convert_lighting_mode
export_settings['gltf_binary'] = bytearray() export_settings['gltf_binary'] = bytearray()
export_settings['gltf_binaryfilename'] = ( export_settings['gltf_binaryfilename'] = (
@ -1043,7 +1045,7 @@ class GLTF_PT_export_data_lighting(bpy.types.Panel):
sfile = context.space_data sfile = context.space_data
operator = sfile.active_operator operator = sfile.active_operator
layout.prop(operator, 'convert_lighting_mode') layout.prop(operator, 'export_import_convert_lighting_mode')
class GLTF_PT_export_data_shapekeys(bpy.types.Panel): class GLTF_PT_export_data_shapekeys(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER' bl_space_type = 'FILE_BROWSER'
@ -1225,11 +1227,38 @@ class GLTF_PT_export_animation(bpy.types.Panel):
row.active = operator.export_morph is True row.active = operator.export_morph is True
row.prop(operator, 'export_morph_animation') row.prop(operator, 'export_morph_animation')
row = layout.row() row = layout.row()
row.active = operator.export_force_sampling row.active = operator.export_force_sampling and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS']
row.prop(operator, 'export_bake_animation') row.prop(operator, 'export_bake_animation')
if operator.export_animation_mode == "SCENE": if operator.export_animation_mode == "SCENE":
layout.prop(operator, 'export_anim_scene_split_object') layout.prop(operator, 'export_anim_scene_split_object')
class GLTF_PT_export_animation_notes(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Notes"
bl_parent_id = "GLTF_PT_export_animation"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENE_OT_gltf" and \
operator.export_animation_mode in ["NLA_TRACKS", "SCENE"]
def draw(self, context):
operator = context.space_data.active_operator
layout = self.layout
if operator.export_animation_mode == "SCENE":
layout.label(text="Scene mode uses full bake mode:")
layout.label(text="- sampling is active")
layout.label(text="- baking all objects is active")
layout.label(text="- Using scene frame range")
elif operator.export_animation_mode == "NLA_TRACKS":
layout.label(text="Track mode uses full bake mode:")
layout.label(text="- sampling is active")
layout.label(text="- baking all objects is active")
class GLTF_PT_export_animation_ranges(bpy.types.Panel): class GLTF_PT_export_animation_ranges(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER' bl_space_type = 'FILE_BROWSER'
@ -1256,8 +1285,12 @@ class GLTF_PT_export_animation_ranges(bpy.types.Panel):
layout.active = operator.export_animations layout.active = operator.export_animations
layout.prop(operator, 'export_current_frame') layout.prop(operator, 'export_current_frame')
layout.prop(operator, 'export_frame_range') row = layout.row()
row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'NLA_TRACKS']
row.prop(operator, 'export_frame_range')
layout.prop(operator, 'export_anim_slide_to_zero') layout.prop(operator, 'export_anim_slide_to_zero')
row = layout.row()
row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'NLA_TRACKS']
layout.prop(operator, 'export_negative_frame') layout.prop(operator, 'export_negative_frame')
class GLTF_PT_export_animation_armature(bpy.types.Panel): class GLTF_PT_export_animation_armature(bpy.types.Panel):
@ -1304,7 +1337,7 @@ class GLTF_PT_export_animation_sampling(bpy.types.Panel):
def draw_header(self, context): def draw_header(self, context):
sfile = context.space_data sfile = context.space_data
operator = sfile.active_operator operator = sfile.active_operator
self.layout.active = operator.export_animations self.layout.active = operator.export_animations and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS']
self.layout.prop(operator, "export_force_sampling", text="") self.layout.prop(operator, "export_force_sampling", text="")
def draw(self, context): def draw(self, context):
@ -1347,11 +1380,9 @@ class GLTF_PT_export_animation_optimize(bpy.types.Panel):
layout.prop(operator, 'export_optimize_animation_size') layout.prop(operator, 'export_optimize_animation_size')
row = layout.row() row = layout.row()
row.active = operator.export_optimize_animation_size
row.prop(operator, 'export_optimize_animation_keep_anim_armature') row.prop(operator, 'export_optimize_animation_keep_anim_armature')
row = layout.row() row = layout.row()
row.active = operator.export_optimize_animation_size
row.prop(operator, 'export_optimize_animation_keep_anim_object') row.prop(operator, 'export_optimize_animation_keep_anim_object')
@ -1489,7 +1520,7 @@ class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper):
layout.prop(self, 'import_shading') layout.prop(self, 'import_shading')
layout.prop(self, 'guess_original_bind_pose') layout.prop(self, 'guess_original_bind_pose')
layout.prop(self, 'bone_heuristic') layout.prop(self, 'bone_heuristic')
layout.prop(self, 'convert_lighting_mode') layout.prop(self, 'export_import_convert_lighting_mode')
def invoke(self, context, event): def invoke(self, context, event):
import sys import sys
@ -1641,6 +1672,7 @@ classes = (
GLTF_PT_export_data_lighting, GLTF_PT_export_data_lighting,
GLTF_PT_export_data_compression, GLTF_PT_export_data_compression,
GLTF_PT_export_animation, GLTF_PT_export_animation,
GLTF_PT_export_animation_notes,
GLTF_PT_export_animation_ranges, GLTF_PT_export_animation_ranges,
GLTF_PT_export_animation_armature, GLTF_PT_export_animation_armature,
GLTF_PT_export_animation_sampling, GLTF_PT_export_animation_sampling,

View File

@ -135,3 +135,10 @@ def get_attribute_type(component_type, data_type):
}[component_type] }[component_type]
else: else:
pass pass
def get_gltf_interpolation(interpolation):
return {
"BEZIER": "CUBICSPLINE",
"LINEAR": "LINEAR",
"CONSTANT": "STEP"
}.get(interpolation, "LINEAR")

View File

@ -143,7 +143,7 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
# Check if the property can be exported without sampling # Check if the property can be exported without sampling
new_properties = {} new_properties = {}
for prop in target_data['properties'].keys(): for prop in target_data['properties'].keys():
if __needs_baking(obj_uuid, target_data['properties'][prop], export_settings) is True: if needs_baking(obj_uuid, target_data['properties'][prop], export_settings) is True:
to_be_sampled.append((obj_uuid, target_data['type'], get_channel_from_target(get_target(prop)), target_data['bone'])) # bone can be None if not a bone :) to_be_sampled.append((obj_uuid, target_data['type'], get_channel_from_target(get_target(prop)), target_data['bone'])) # bone can be None if not a bone :)
else: else:
new_properties[prop] = target_data['properties'][prop] new_properties[prop] = target_data['properties'][prop]
@ -262,7 +262,7 @@ def __gather_sampler(obj_uuid: str,
return gather_animation_fcurves_sampler(obj_uuid, channel_group, bone, custom_range, export_settings) return gather_animation_fcurves_sampler(obj_uuid, channel_group, bone, custom_range, export_settings)
def __needs_baking(obj_uuid: str, def needs_baking(obj_uuid: str,
channels: typing.Tuple[bpy.types.FCurve], channels: typing.Tuple[bpy.types.FCurve],
export_settings export_settings
) -> bool: ) -> bool:

View File

@ -6,6 +6,7 @@ import typing
import mathutils import mathutils
from .....io.com import gltf2_io from .....io.com import gltf2_io
from .....io.com import gltf2_io_constants from .....io.com import gltf2_io_constants
from .....blender.com.gltf2_blender_conversion import get_gltf_interpolation
from .....io.exp import gltf2_io_binary_data from .....io.exp import gltf2_io_binary_data
from .....io.exp.gltf2_io_user_extensions import export_user_extensions from .....io.exp.gltf2_io_user_extensions import export_user_extensions
from ....com.gltf2_blender_data_path import get_target_property_name from ....com.gltf2_blender_data_path import get_target_property_name
@ -205,8 +206,4 @@ def __gather_interpolation(
blender_keyframe = [c for c in channel_group if c is not None][0].keyframe_points[0] blender_keyframe = [c for c in channel_group if c is not None][0].keyframe_points[0]
# Select the interpolation method. # Select the interpolation method.
return { return get_gltf_interpolation(blender_keyframe.interpolation)
"BEZIER": "CUBICSPLINE",
"LINEAR": "LINEAR",
"CONSTANT": "STEP"
}[blender_keyframe.interpolation]

View File

@ -6,6 +6,7 @@ import typing
from ....io.com import gltf2_io from ....io.com import gltf2_io
from ....io.com.gltf2_io_debug import print_console from ....io.com.gltf2_io_debug import print_console
from ....io.exp.gltf2_io_user_extensions import export_user_extensions from ....io.exp.gltf2_io_user_extensions import export_user_extensions
from ....blender.com.gltf2_blender_conversion import get_gltf_interpolation
from ...com.gltf2_blender_data_path import is_bone_anim_channel from ...com.gltf2_blender_data_path import is_bone_anim_channel
from ...com.gltf2_blender_extras import generate_extras from ...com.gltf2_blender_extras import generate_extras
from ..gltf2_blender_gather_cache import cached from ..gltf2_blender_gather_cache import cached
@ -69,9 +70,18 @@ def prepare_actions_range(export_settings):
blender_actions = __get_blender_actions(obj_uuid, export_settings) blender_actions = __get_blender_actions(obj_uuid, export_settings)
for blender_action, track, type_ in blender_actions: for blender_action, track, type_ in blender_actions:
# What about frame_range bug for single keyframe animations ? 107030
start_frame = int(blender_action.frame_range[0]) start_frame = int(blender_action.frame_range[0])
end_frame = int(blender_action.frame_range[1]) end_frame = int(blender_action.frame_range[1])
if end_frame - start_frame == 1:
# To workaround Blender bug 107030, check manually
try: # Avoid crash in case of strange/buggy fcurves
start_frame = int(min([c.range()[0] for c in blender_action.fcurves]))
end_frame = int(max([c.range()[1] for c in blender_action.fcurves]))
except:
pass
export_settings['ranges'][obj_uuid][blender_action.name] = {} export_settings['ranges'][obj_uuid][blender_action.name] = {}
# If some negative frame and crop -> set start at 0 # If some negative frame and crop -> set start at 0
@ -277,9 +287,9 @@ def gather_action_animations( obj_uuid: int,
animation, to_be_sampled = gather_animation_fcurves(obj_uuid, blender_action, export_settings) animation, to_be_sampled = gather_animation_fcurves(obj_uuid, blender_action, export_settings)
for (obj_uuid, type_, prop, bone) in to_be_sampled: for (obj_uuid, type_, prop, bone) in to_be_sampled:
if type_ == "BONE": if type_ == "BONE":
channel = gather_sampled_bone_channel(obj_uuid, bone, prop, blender_action.name, True, export_settings) channel = gather_sampled_bone_channel(obj_uuid, bone, prop, blender_action.name, True, get_gltf_interpolation("LINEAR"), export_settings)
elif type_ == "OBJECT": elif type_ == "OBJECT":
channel = gather_sampled_object_channel(obj_uuid, prop, blender_action.name, True, export_settings) channel = gather_sampled_object_channel(obj_uuid, prop, blender_action.name, True, get_gltf_interpolation("LINEAR"), export_settings)
elif type_ == "SK": elif type_ == "SK":
channel = gather_sampled_sk_channel(obj_uuid, blender_action.name, export_settings) channel = gather_sampled_sk_channel(obj_uuid, blender_action.name, export_settings)
else: else:

View File

@ -5,8 +5,10 @@ import bpy
import typing import typing
from ......io.com import gltf2_io from ......io.com import gltf2_io
from ......io.exp.gltf2_io_user_extensions import export_user_extensions from ......io.exp.gltf2_io_user_extensions import export_user_extensions
from ......blender.com.gltf2_blender_conversion import get_gltf_interpolation
from .....com.gltf2_blender_conversion import get_target, get_channel_from_target from .....com.gltf2_blender_conversion import get_target, get_channel_from_target
from ...fcurves.gltf2_blender_gather_fcurves_channels import get_channel_groups from ...fcurves.gltf2_blender_gather_fcurves_channels import get_channel_groups
from ...fcurves.gltf2_blender_gather_fcurves_channels import needs_baking
from ...gltf2_blender_gather_drivers import get_sk_drivers from ...gltf2_blender_gather_drivers import get_sk_drivers
from ..object.gltf2_blender_gather_object_channels import gather_sampled_object_channel from ..object.gltf2_blender_gather_object_channels import gather_sampled_object_channel
from ..shapekeys.gltf2_blender_gather_sk_channels import gather_sampled_sk_channel from ..shapekeys.gltf2_blender_gather_sk_channels import gather_sampled_sk_channel
@ -22,16 +24,27 @@ def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_
bones_to_be_animated = [export_settings["vtree"].nodes[b].blender_bone.name for b in bones_uuid] bones_to_be_animated = [export_settings["vtree"].nodes[b].blender_bone.name for b in bones_uuid]
# List of really animated bones is needed for optimization decision # List of really animated bones is needed for optimization decision
list_of_animated_bone_channels = [] list_of_animated_bone_channels = {}
if armature_uuid != blender_action_name and blender_action_name in bpy.data.actions: if armature_uuid != blender_action_name and blender_action_name in bpy.data.actions:
# Not bake situation # Not bake situation
channels_animated, to_be_sampled = get_channel_groups(armature_uuid, bpy.data.actions[blender_action_name], export_settings) channels_animated, to_be_sampled = get_channel_groups(armature_uuid, bpy.data.actions[blender_action_name], export_settings)
for chan in [chan for chan in channels_animated.values() if chan['bone'] is not None]: for chan in [chan for chan in channels_animated.values() if chan['bone'] is not None]:
for prop in chan['properties'].keys(): for prop in chan['properties'].keys():
list_of_animated_bone_channels.append((chan['bone'], get_channel_from_target(get_target(prop)))) list_of_animated_bone_channels[
(
chan['bone'],
get_channel_from_target(get_target(prop))
)
] = get_gltf_interpolation(chan['properties'][prop][0].keyframe_points[0].interpolation) # Could be exported without sampling : keep interpolation
for _, _, chan_prop, chan_bone in [chan for chan in to_be_sampled if chan[1] == "BONE"]: for _, _, chan_prop, chan_bone in [chan for chan in to_be_sampled if chan[1] == "BONE"]:
list_of_animated_bone_channels.append((chan_bone, chan_prop)) list_of_animated_bone_channels[
(
chan_bone,
chan_prop,
)
] = get_gltf_interpolation("LINEAR") # if forced to be sampled, keep LINEAR interpolation
for bone in bones_to_be_animated: for bone in bones_to_be_animated:
for p in ["location", "rotation_quaternion", "scale"]: for p in ["location", "rotation_quaternion", "scale"]:
@ -40,7 +53,8 @@ def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_
bone, bone,
p, p,
blender_action_name, blender_action_name,
(bone, p) in list_of_animated_bone_channels, (bone, p) in list_of_animated_bone_channels.keys(),
list_of_animated_bone_channels[(bone, p)] if (bone, p) in list_of_animated_bone_channels.keys() else get_gltf_interpolation("LINEAR"),
export_settings) export_settings)
if channel is not None: if channel is not None:
channels.append(channel) channels.append(channel)
@ -48,15 +62,17 @@ def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_
# Retrieve animation on armature object itself, if any # Retrieve animation on armature object itself, if any
# If armature is baked (no animation of armature), need to use all channels # If armature is baked (no animation of armature), need to use all channels
if blender_action_name == armature_uuid or export_settings['gltf_animation_mode'] in ["SCENE", "NLA_TRACKS"]: if blender_action_name == armature_uuid or export_settings['gltf_animation_mode'] in ["SCENE", "NLA_TRACKS"]:
armature_channels = ["location", "rotation_quaternion", "scale"] armature_channels = []
else: else:
armature_channels = __gather_armature_object_channel(bpy.data.actions[blender_action_name], export_settings) armature_channels = __gather_armature_object_channel(armature_uuid, bpy.data.actions[blender_action_name], export_settings)
for channel in armature_channels:
for p in ["location", "rotation_quaternion", "scale"]:
armature_channel = gather_sampled_object_channel( armature_channel = gather_sampled_object_channel(
armature_uuid, armature_uuid,
channel, p,
blender_action_name, blender_action_name,
True, # channel is animated (because we detect it on __gather_armature_object_channel) p in [a[0] for a in armature_channels],
[c[1] for c in armature_channels if c[0] == p][0] if p in [a[0] for a in armature_channels] else "LINEAR",
export_settings export_settings
) )
@ -79,12 +95,13 @@ def gather_sampled_bone_channel(
channel: str, channel: str,
action_name: str, action_name: str,
node_channel_is_animated: bool, node_channel_is_animated: bool,
node_channel_interpolation: str,
export_settings export_settings
): ):
__target= __gather_target(armature_uuid, bone, channel, export_settings) __target= __gather_target(armature_uuid, bone, channel, export_settings)
if __target.path is not None: if __target.path is not None:
sampler = __gather_sampler(armature_uuid, bone, channel, action_name, node_channel_is_animated, export_settings) sampler = __gather_sampler(armature_uuid, bone, channel, action_name, node_channel_is_animated, node_channel_interpolation, export_settings)
if sampler is None: if sampler is None:
# After check, no need to animate this node for this channel # After check, no need to animate this node for this channel
@ -120,30 +137,61 @@ def __gather_target(armature_uuid: str,
return gather_armature_sampled_channel_target( return gather_armature_sampled_channel_target(
armature_uuid, bone, channel, export_settings) armature_uuid, bone, channel, export_settings)
def __gather_sampler(armature_uuid, bone, channel, action_name, node_channel_is_animated, export_settings): def __gather_sampler(armature_uuid, bone, channel, action_name, node_channel_is_animated, node_channel_interpolation, export_settings):
return gather_bone_sampled_animation_sampler( return gather_bone_sampled_animation_sampler(
armature_uuid, armature_uuid,
bone, bone,
channel, channel,
action_name, action_name,
node_channel_is_animated, node_channel_is_animated,
node_channel_interpolation,
export_settings export_settings
) )
def __gather_armature_object_channel(blender_action: str, export_settings): def __gather_armature_object_channel(obj_uuid: str, blender_action, export_settings):
channels = [] channels = []
for p in ["location", "rotation_quaternion", "scale", "delta_location", "delta_scale", "delta_rotation_euler", "delta_rotation_quaternion"]:
if p in [f.data_path for f in blender_action.fcurves]: channels_animated, to_be_sampled = get_channel_groups(obj_uuid, blender_action, export_settings)
# Remove all channel linked to bones, keep only directly object channels
channels_animated = [c for c in channels_animated.values() if c['type'] == "OBJECT"]
to_be_sampled = [c for c in to_be_sampled if c[1] == "OBJECT"]
original_channels = []
for c in channels_animated:
original_channels.extend([(prop, c['properties'][prop][0].keyframe_points[0].interpolation) for prop in c['properties'].keys()])
for c, inter in original_channels:
channels.append( channels.append(
(
{ {
"location":"location", "location":"location",
"rotation_quaternion": "rotation_quaternion", "rotation_quaternion": "rotation_quaternion",
"rotation_euler": "rotation_quaternion",
"scale": "scale", "scale": "scale",
"delta_location": "location", "delta_location": "location",
"delta_scale": "scale", "delta_scale": "scale",
"delta_rotation_euler": "rotation_quaternion", "delta_rotation_euler": "rotation_quaternion",
"delta_rotation_quaternion": "rotation_quaternion" "delta_rotation_quaternion": "rotation_quaternion"
}.get(p) }.get(c),
get_gltf_interpolation(inter)
)
) )
return list(set(channels)) #remove doubles for c in to_be_sampled:
channels.append(
(
{
"location":"location",
"rotation_quaternion": "rotation_quaternion",
"rotation_euler": "rotation_quaternion",
"scale": "scale",
"delta_location": "location",
"delta_scale": "scale",
"delta_rotation_euler": "rotation_quaternion",
"delta_rotation_quaternion": "rotation_quaternion"
}.get(c[2]),
get_gltf_interpolation("LINEAR") # Forced to be sampled, so use LINEAR
)
)
return channels

View File

@ -53,8 +53,21 @@ def gather_bone_sampled_keyframes(
return None return None
if not export_settings['gltf_optimize_animation']: if not export_settings['gltf_optimize_animation']:
# For bones, if all values are the same, keeping only if changing values, or if user want to keep data
if node_channel_is_animated is True:
return keyframes # Always keeping
else:
# baked bones
if export_settings['gltf_optimize_animation_keep_armature'] is False:
# Not keeping if not changing property
cst = fcurve_is_constant(keyframes)
return None if cst is True else keyframes
else:
# Keep data, as requested by user. We keep all samples, as user don't want to optimize
return keyframes return keyframes
else:
# For armatures # For armatures
# Check if all values are the same # Check if all values are the same
# In that case, if there is no real keyframe on this channel for this given bone, # In that case, if there is no real keyframe on this channel for this given bone,

View File

@ -21,6 +21,7 @@ def gather_bone_sampled_animation_sampler(
channel: str, channel: str,
action_name: str, action_name: str,
node_channel_is_animated: bool, node_channel_is_animated: bool,
node_channel_interpolation: str,
export_settings export_settings
): ):
@ -45,7 +46,7 @@ def gather_bone_sampled_animation_sampler(
extensions=None, extensions=None,
extras=None, extras=None,
input=input, input=input,
interpolation=__gather_interpolation(export_settings), interpolation=__gather_interpolation(node_channel_is_animated, node_channel_interpolation, keyframes, export_settings),
output=output output=output
) )
@ -194,6 +195,25 @@ def __convert_keyframes(armature_uuid, bone_name, channel, keyframes, action_nam
return input, output return input, output
def __gather_interpolation(export_settings): def __gather_interpolation(node_channel_is_animated, node_channel_interpolation, keyframes, export_settings):
# TODO: check if the bone was animated with CONSTANT
return 'LINEAR' if len(keyframes) > 2:
# keep STEP as STEP, other become LINEAR
return {
"STEP": "STEP"
}.get(node_channel_interpolation, "LINEAR")
elif len(keyframes) == 1:
if node_channel_is_animated is False:
return "STEP"
else:
return node_channel_interpolation
else:
# If we only have 2 keyframes, set interpolation to STEP if baked
if node_channel_is_animated is False:
# baked => We have first and last keyframe
return "STEP"
else:
if keyframes[0].value == keyframes[1].value:
return "STEP"
else:
return "LINEAR"

View File

@ -5,6 +5,7 @@ import bpy
import typing import typing
from ......io.com import gltf2_io from ......io.com import gltf2_io
from ......io.exp.gltf2_io_user_extensions import export_user_extensions from ......io.exp.gltf2_io_user_extensions import export_user_extensions
from ......blender.com.gltf2_blender_conversion import get_gltf_interpolation
from .....com.gltf2_blender_conversion import get_target, get_channel_from_target from .....com.gltf2_blender_conversion import get_target, get_channel_from_target
from ....gltf2_blender_gather_cache import cached from ....gltf2_blender_gather_cache import cached
from ...fcurves.gltf2_blender_gather_fcurves_channels import get_channel_groups from ...fcurves.gltf2_blender_gather_fcurves_channels import get_channel_groups
@ -14,23 +15,26 @@ from .gltf2_blender_gather_object_channel_target import gather_object_sampled_ch
def gather_object_sampled_channels(object_uuid: str, blender_action_name: str, export_settings) -> typing.List[gltf2_io.AnimationChannel]: def gather_object_sampled_channels(object_uuid: str, blender_action_name: str, export_settings) -> typing.List[gltf2_io.AnimationChannel]:
channels = [] channels = []
list_of_animated_channels = [] list_of_animated_channels = {}
if object_uuid != blender_action_name and blender_action_name in bpy.data.actions: if object_uuid != blender_action_name and blender_action_name in bpy.data.actions:
# Not bake situation # Not bake situation
channels_animated, to_be_sampled = get_channel_groups(object_uuid, bpy.data.actions[blender_action_name], export_settings) channels_animated, to_be_sampled = get_channel_groups(object_uuid, bpy.data.actions[blender_action_name], export_settings)
for chan in [chan for chan in channels_animated.values() if chan['bone'] is None]: for chan in [chan for chan in channels_animated.values() if chan['bone'] is None]:
for prop in chan['properties'].keys(): for prop in chan['properties'].keys():
list_of_animated_channels.append(get_channel_from_target(get_target(prop))) list_of_animated_channels[
get_channel_from_target(get_target(prop))
] = get_gltf_interpolation(chan['properties'][prop][0].keyframe_points[0].interpolation) # Could be exported without sampling : keep interpolation
for _, _, chan_prop, _ in [chan for chan in to_be_sampled if chan[1] == "OBJECT"]: for _, _, chan_prop, _ in [chan for chan in to_be_sampled if chan[1] == "OBJECT"]:
list_of_animated_channels.append(chan_prop) list_of_animated_channels[chan_prop] = get_gltf_interpolation("LINEAR") # if forced to be sampled, keep LINEAR interpolation
for p in ["location", "rotation_quaternion", "scale"]: for p in ["location", "rotation_quaternion", "scale"]:
channel = gather_sampled_object_channel( channel = gather_sampled_object_channel(
object_uuid, object_uuid,
p, p,
blender_action_name, blender_action_name,
p in list_of_animated_channels, p in list_of_animated_channels.keys(),
list_of_animated_channels[p] if p in list_of_animated_channels.keys() else get_gltf_interpolation("LINEAR"),
export_settings export_settings
) )
if channel is not None: if channel is not None:
@ -48,12 +52,13 @@ def gather_sampled_object_channel(
channel: str, channel: str,
action_name: str, action_name: str,
node_channel_is_animated: bool, node_channel_is_animated: bool,
node_channel_interpolation: str,
export_settings export_settings
): ):
__target= __gather_target(obj_uuid, channel, export_settings) __target= __gather_target(obj_uuid, channel, export_settings)
if __target.path is not None: if __target.path is not None:
sampler = __gather_sampler(obj_uuid, channel, action_name, node_channel_is_animated, export_settings) sampler = __gather_sampler(obj_uuid, channel, action_name, node_channel_is_animated, node_channel_interpolation, export_settings)
if sampler is None: if sampler is None:
# After check, no need to animate this node for this channel # After check, no need to animate this node for this channel
@ -92,6 +97,7 @@ def __gather_sampler(
channel: str, channel: str,
action_name: str, action_name: str,
node_channel_is_animated: bool, node_channel_is_animated: bool,
node_channel_interpolation: str,
export_settings): export_settings):
@ -100,5 +106,6 @@ def __gather_sampler(
channel, channel,
action_name, action_name,
node_channel_is_animated, node_channel_is_animated,
node_channel_interpolation,
export_settings export_settings
) )

View File

@ -2,6 +2,7 @@
# Copyright 2018-2022 The glTF-Blender-IO authors. # Copyright 2018-2022 The glTF-Blender-IO authors.
import numpy as np import numpy as np
from ....gltf2_blender_gather_tree import VExportNode
from ....gltf2_blender_gather_cache import cached from ....gltf2_blender_gather_cache import cached
from ...gltf2_blender_gather_keyframes import Keyframe from ...gltf2_blender_gather_keyframes import Keyframe
from ..gltf2_blender_gather_animation_sampling_cache import get_cache_data from ..gltf2_blender_gather_animation_sampling_cache import get_cache_data
@ -51,8 +52,21 @@ def gather_object_sampled_keyframes(
return None return None
if not export_settings['gltf_optimize_animation']: if not export_settings['gltf_optimize_animation']:
# For objects, if all values are the same, keeping only if changing values, or if user want to keep data
if node_channel_is_animated is True:
return keyframes # Always keeping
else:
# baked object
if export_settings['gltf_optimize_animation_keep_object'] is False:
# Not keeping if not changing property
cst = fcurve_is_constant(keyframes)
return None if cst is True else keyframes
else:
# Keep data, as requested by user. We keep all samples, as user don't want to optimize
return keyframes return keyframes
else:
# For objects, if all values are the same, we keep only first and last # For objects, if all values are the same, we keep only first and last
cst = fcurve_is_constant(keyframes) cst = fcurve_is_constant(keyframes)
if node_channel_is_animated is True: if node_channel_is_animated is True:

View File

@ -20,6 +20,7 @@ def gather_object_sampled_animation_sampler(
channel: str, channel: str,
action_name: str, action_name: str,
node_channel_is_animated: bool, node_channel_is_animated: bool,
node_channel_interpolation: str,
export_settings export_settings
): ):
@ -41,7 +42,7 @@ def gather_object_sampled_animation_sampler(
extensions=None, extensions=None,
extras=None, extras=None,
input=input, input=input,
interpolation=__gather_interpolation(export_settings), interpolation=__gather_interpolation(node_channel_is_animated, node_channel_interpolation, keyframes, export_settings),
output=output output=output
) )
@ -66,10 +67,6 @@ def __gather_keyframes(
export_settings export_settings
) )
if keyframes is None:
# After check, no need to animation this node
return None
return keyframes return keyframes
def __convert_keyframes(obj_uuid: str, channel: str, keyframes, action_name: str, export_settings): def __convert_keyframes(obj_uuid: str, channel: str, keyframes, action_name: str, export_settings):
@ -136,6 +133,29 @@ def __convert_keyframes(obj_uuid: str, channel: str, keyframes, action_name: str
return input, output return input, output
def __gather_interpolation(export_settings): def __gather_interpolation(
# TODO: check if the object was animated with CONSTANT node_channel_is_animated: bool,
return 'LINEAR' node_channel_interpolation: str,
keyframes,
export_settings):
if len(keyframes) > 2:
# keep STEP as STEP, other become LINEAR
return {
"STEP": "STEP"
}.get(node_channel_interpolation, "LINEAR")
elif len(keyframes) == 1:
if node_channel_is_animated is False:
return "STEP"
else:
return node_channel_interpolation
else:
# If we only have 2 keyframes, set interpolation to STEP if baked
if node_channel_is_animated is False:
# baked => We have first and last keyframe
return "STEP"
else:
if keyframes[0].value == keyframes[1].value:
return "STEP"
else:
return "LINEAR"

View File

@ -14,7 +14,6 @@ def get_mesh_cache_key(blender_mesh,
blender_object, blender_object,
vertex_groups, vertex_groups,
modifiers, modifiers,
skip_filter,
materials, materials,
original_mesh, original_mesh,
export_settings): export_settings):
@ -34,21 +33,19 @@ def get_mesh_cache_key(blender_mesh,
return ( return (
(id(mesh_to_id_cache),), (id(mesh_to_id_cache),),
(modifiers,), (modifiers,),
(skip_filter,), #TODO to check if still needed
mats mats
) )
@cached_by_key(key=get_mesh_cache_key) @cached_by_key(key=get_mesh_cache_key)
def gather_mesh(blender_mesh: bpy.types.Mesh, def gather_mesh(blender_mesh: bpy.types.Mesh,
uuid_for_skined_data, uuid_for_skined_data,
vertex_groups: Optional[bpy.types.VertexGroups], vertex_groups: bpy.types.VertexGroups,
modifiers: Optional[bpy.types.ObjectModifiers], modifiers: Optional[bpy.types.ObjectModifiers],
skip_filter: bool,
materials: Tuple[bpy.types.Material], materials: Tuple[bpy.types.Material],
original_mesh: bpy.types.Mesh, original_mesh: bpy.types.Mesh,
export_settings export_settings
) -> Optional[gltf2_io.Mesh]: ) -> Optional[gltf2_io.Mesh]:
if not skip_filter and not __filter_mesh(blender_mesh, vertex_groups, modifiers, export_settings): if not __filter_mesh(blender_mesh, vertex_groups, modifiers, export_settings):
return None return None
mesh = gltf2_io.Mesh( mesh = gltf2_io.Mesh(
@ -75,25 +72,21 @@ def gather_mesh(blender_mesh: bpy.types.Mesh,
blender_object, blender_object,
vertex_groups, vertex_groups,
modifiers, modifiers,
skip_filter,
materials) materials)
return mesh return mesh
def __filter_mesh(blender_mesh: bpy.types.Mesh, def __filter_mesh(blender_mesh: bpy.types.Mesh,
vertex_groups: Optional[bpy.types.VertexGroups], vertex_groups: bpy.types.VertexGroups,
modifiers: Optional[bpy.types.ObjectModifiers], modifiers: Optional[bpy.types.ObjectModifiers],
export_settings export_settings
) -> bool: ) -> bool:
if blender_mesh.users == 0:
return False
return True return True
def __gather_extensions(blender_mesh: bpy.types.Mesh, def __gather_extensions(blender_mesh: bpy.types.Mesh,
vertex_groups: Optional[bpy.types.VertexGroups], vertex_groups: bpy.types.VertexGroups,
modifiers: Optional[bpy.types.ObjectModifiers], modifiers: Optional[bpy.types.ObjectModifiers],
export_settings export_settings
) -> Any: ) -> Any:
@ -101,7 +94,7 @@ def __gather_extensions(blender_mesh: bpy.types.Mesh,
def __gather_extras(blender_mesh: bpy.types.Mesh, def __gather_extras(blender_mesh: bpy.types.Mesh,
vertex_groups: Optional[bpy.types.VertexGroups], vertex_groups: bpy.types.VertexGroups,
modifiers: Optional[bpy.types.ObjectModifiers], modifiers: Optional[bpy.types.ObjectModifiers],
export_settings export_settings
) -> Optional[Dict[Any, Any]]: ) -> Optional[Dict[Any, Any]]:
@ -128,7 +121,7 @@ def __gather_extras(blender_mesh: bpy.types.Mesh,
def __gather_name(blender_mesh: bpy.types.Mesh, def __gather_name(blender_mesh: bpy.types.Mesh,
vertex_groups: Optional[bpy.types.VertexGroups], vertex_groups: bpy.types.VertexGroups,
modifiers: Optional[bpy.types.ObjectModifiers], modifiers: Optional[bpy.types.ObjectModifiers],
export_settings export_settings
) -> str: ) -> str:
@ -137,7 +130,7 @@ def __gather_name(blender_mesh: bpy.types.Mesh,
def __gather_primitives(blender_mesh: bpy.types.Mesh, def __gather_primitives(blender_mesh: bpy.types.Mesh,
uuid_for_skined_data, uuid_for_skined_data,
vertex_groups: Optional[bpy.types.VertexGroups], vertex_groups: bpy.types.VertexGroups,
modifiers: Optional[bpy.types.ObjectModifiers], modifiers: Optional[bpy.types.ObjectModifiers],
materials: Tuple[bpy.types.Material], materials: Tuple[bpy.types.Material],
export_settings export_settings
@ -151,7 +144,7 @@ def __gather_primitives(blender_mesh: bpy.types.Mesh,
def __gather_weights(blender_mesh: bpy.types.Mesh, def __gather_weights(blender_mesh: bpy.types.Mesh,
vertex_groups: Optional[bpy.types.VertexGroups], vertex_groups: bpy.types.VertexGroups,
modifiers: Optional[bpy.types.ObjectModifiers], modifiers: Optional[bpy.types.ObjectModifiers],
export_settings export_settings
) -> Optional[List[float]]: ) -> Optional[List[float]]:

View File

@ -182,11 +182,7 @@ def __gather_mesh(vnode, blender_object, export_settings):
# Be sure that object is valid (no NaN for example) # Be sure that object is valid (no NaN for example)
blender_object.data.validate() blender_object.data.validate()
# If not using vertex group, they are irrelevant for caching --> ensure that they do not trigger a cache miss
vertex_groups = blender_object.vertex_groups
modifiers = blender_object.modifiers modifiers = blender_object.modifiers
if len(vertex_groups) == 0:
vertex_groups = None
if len(modifiers) == 0: if len(modifiers) == 0:
modifiers = None modifiers = None
@ -194,7 +190,9 @@ def __gather_mesh(vnode, blender_object, export_settings):
if export_settings['gltf_apply']: if export_settings['gltf_apply']:
if modifiers is None: # If no modifier, use original mesh, it will instance all shared mesh in a single glTF mesh if modifiers is None: # If no modifier, use original mesh, it will instance all shared mesh in a single glTF mesh
blender_mesh = blender_object.data blender_mesh = blender_object.data
skip_filter = False # Keep materials from object, as no modifiers are applied, so no risk that
# modifiers changed them
materials = tuple(ms.material for ms in blender_object.material_slots)
else: else:
armature_modifiers = {} armature_modifiers = {}
if export_settings['gltf_skins']: if export_settings['gltf_skins']:
@ -209,25 +207,27 @@ def __gather_mesh(vnode, blender_object, export_settings):
blender_mesh = blender_mesh_owner.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph) blender_mesh = blender_mesh_owner.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
for prop in blender_object.data.keys(): for prop in blender_object.data.keys():
blender_mesh[prop] = blender_object.data[prop] blender_mesh[prop] = blender_object.data[prop]
skip_filter = True
if export_settings['gltf_skins']: if export_settings['gltf_skins']:
# restore Armature modifiers # restore Armature modifiers
for idx, show_viewport in armature_modifiers.items(): for idx, show_viewport in armature_modifiers.items():
blender_object.modifiers[idx].show_viewport = show_viewport blender_object.modifiers[idx].show_viewport = show_viewport
# Keep materials from the newly created tmp mesh
materials = tuple(mat for mat in blender_mesh.materials)
if len(materials) == 1 and materials[0] is None:
materials = tuple(ms.material for ms in blender_object.material_slots)
else: else:
blender_mesh = blender_object.data blender_mesh = blender_object.data
skip_filter = False
# If no skin are exported, no need to have vertex group, this will create a cache miss # If no skin are exported, no need to have vertex group, this will create a cache miss
if not export_settings['gltf_skins']: if not export_settings['gltf_skins']:
vertex_groups = None
modifiers = None modifiers = None
else: else:
# Check if there is an armature modidier # Check if there is an armature modidier
if len([mod for mod in blender_object.modifiers if mod.type == "ARMATURE"]) == 0: if len([mod for mod in blender_object.modifiers if mod.type == "ARMATURE"]) == 0:
vertex_groups = None # Not needed if no armature, avoid a cache miss
modifiers = None modifiers = None
# Keep materials from object, as no modifiers are applied, so no risk that
# modifiers changed them
materials = tuple(ms.material for ms in blender_object.material_slots) materials = tuple(ms.material for ms in blender_object.material_slots)
# retrieve armature # retrieve armature
@ -241,9 +241,8 @@ def __gather_mesh(vnode, blender_object, export_settings):
result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh, result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh,
uuid_for_skined_data, uuid_for_skined_data,
vertex_groups, blender_object.vertex_groups,
modifiers, modifiers,
skip_filter,
materials, materials,
None, None,
export_settings) export_settings)
@ -279,17 +278,14 @@ def __gather_mesh_from_nonmesh(blender_object, export_settings):
needs_to_mesh_clear = True needs_to_mesh_clear = True
skip_filter = True
materials = tuple([ms.material for ms in blender_object.material_slots if ms.material is not None]) materials = tuple([ms.material for ms in blender_object.material_slots if ms.material is not None])
vertex_groups = None
modifiers = None modifiers = None
blender_object_for_skined_data = None blender_object_for_skined_data = None
result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh, result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh,
blender_object_for_skined_data, blender_object_for_skined_data,
vertex_groups, blender_object.vertex_groups,
modifiers, modifiers,
skip_filter,
materials, materials,
blender_object.data, blender_object.data,
export_settings) export_settings)
@ -361,8 +357,7 @@ def gather_skin(vnode, export_settings):
return None return None
# no skin needed when the modifier is linked without having a vertex group # no skin needed when the modifier is linked without having a vertex group
vertex_groups = blender_object.vertex_groups if len(blender_object.vertex_groups) == 0:
if len(vertex_groups) == 0:
return None return None
# check if any vertices in the mesh are part of a vertex group # check if any vertices in the mesh are part of a vertex group

View File

@ -15,9 +15,9 @@ from .material import gltf2_blender_gather_materials
from .material.extensions import gltf2_blender_gather_materials_variants from .material.extensions import gltf2_blender_gather_materials_variants
@cached @cached
def get_primitive_cache_key( def gather_primitive_cache_key(
blender_mesh, blender_mesh,
blender_object, uuid_for_skined_data,
vertex_groups, vertex_groups,
modifiers, modifiers,
materials, materials,
@ -36,11 +36,11 @@ def get_primitive_cache_key(
) )
@cached_by_key(key=get_primitive_cache_key) @cached_by_key(key=gather_primitive_cache_key)
def gather_primitives( def gather_primitives(
blender_mesh: bpy.types.Mesh, blender_mesh: bpy.types.Mesh,
uuid_for_skined_data, uuid_for_skined_data,
vertex_groups: Optional[bpy.types.VertexGroups], vertex_groups: bpy.types.VertexGroups,
modifiers: Optional[bpy.types.ObjectModifiers], modifiers: Optional[bpy.types.ObjectModifiers],
materials: Tuple[bpy.types.Material], materials: Tuple[bpy.types.Material],
export_settings export_settings
@ -92,11 +92,33 @@ def gather_primitives(
return primitives return primitives
@cached @cached
def get_primitive_cache_key(
blender_mesh,
uuid_for_skined_data,
vertex_groups,
modifiers,
export_settings):
# Use id of mesh
# Do not use bpy.types that can be unhashable
# Do not use mesh name, that can be not unique (when linked)
# Do not use materials here
# TODO check what is really needed for modifiers
return (
(id(blender_mesh),),
(modifiers,)
)
@cached_by_key(key=get_primitive_cache_key)
def __gather_cache_primitives( def __gather_cache_primitives(
blender_mesh: bpy.types.Mesh, blender_mesh: bpy.types.Mesh,
uuid_for_skined_data, uuid_for_skined_data,
vertex_groups: Optional[bpy.types.VertexGroups], vertex_groups: bpy.types.VertexGroups,
modifiers: Optional[bpy.types.ObjectModifiers], modifiers: Optional[bpy.types.ObjectModifiers],
export_settings export_settings
) -> List[dict]: ) -> List[dict]:

View File

@ -85,7 +85,7 @@ class PrimitiveCreator:
# Check if we have to export skin # Check if we have to export skin
self.armature = None self.armature = None
self.skin = None self.skin = None
if self.blender_vertex_groups and self.export_settings['gltf_skins']: if self.export_settings['gltf_skins']:
if self.modifiers is not None: if self.modifiers is not None:
modifiers_dict = {m.type: m for m in self.modifiers} modifiers_dict = {m.type: m for m in self.modifiers}
if "ARMATURE" in modifiers_dict: if "ARMATURE" in modifiers_dict:
@ -197,15 +197,6 @@ class PrimitiveCreator:
attr['skip_getting_to_dots'] = True attr['skip_getting_to_dots'] = True
self.blender_attributes.append(attr) self.blender_attributes.append(attr)
# Manage uvs TEX_COORD_x
for tex_coord_i in range(self.tex_coord_max):
attr = {}
attr['blender_data_type'] = 'FLOAT2'
attr['blender_domain'] = 'CORNER'
attr['gltf_attribute_name'] = 'TEXCOORD_' + str(tex_coord_i)
attr['get'] = self.get_function()
self.blender_attributes.append(attr)
# Manage NORMALS # Manage NORMALS
if self.use_normals: if self.use_normals:
attr = {} attr = {}
@ -216,6 +207,15 @@ class PrimitiveCreator:
attr['get'] = self.get_function() attr['get'] = self.get_function()
self.blender_attributes.append(attr) self.blender_attributes.append(attr)
# Manage uvs TEX_COORD_x
for tex_coord_i in range(self.tex_coord_max):
attr = {}
attr['blender_data_type'] = 'FLOAT2'
attr['blender_domain'] = 'CORNER'
attr['gltf_attribute_name'] = 'TEXCOORD_' + str(tex_coord_i)
attr['get'] = self.get_function()
self.blender_attributes.append(attr)
# Manage TANGENT # Manage TANGENT
if self.use_tangents: if self.use_tangents:
attr = {} attr = {}
@ -269,6 +269,13 @@ class PrimitiveCreator:
attr['len'] = gltf2_blender_conversion.get_data_length(attr['blender_data_type']) attr['len'] = gltf2_blender_conversion.get_data_length(attr['blender_data_type'])
attr['type'] = gltf2_blender_conversion.get_numpy_type(attr['blender_data_type']) attr['type'] = gltf2_blender_conversion.get_numpy_type(attr['blender_data_type'])
# Now we have all attribtues, we can change order if we want
# Note that the glTF specification doesn't say anything about order
# Attributes are defined only by name
# But if user want it in a particular order, he can use this hook to perform it
export_user_extensions('gather_attributes_change', self.export_settings, self.blender_attributes)
def create_dots_data_structure(self): def create_dots_data_structure(self):
# Now that we get all attributes that are going to be exported, create numpy array that will store them # Now that we get all attributes that are going to be exported, create numpy array that will store them
dot_fields = [('vertex_index', np.uint32)] dot_fields = [('vertex_index', np.uint32)]
@ -698,6 +705,8 @@ class PrimitiveCreator:
self.normals = self.normals.reshape(len(self.blender_mesh.loops), 3) self.normals = self.normals.reshape(len(self.blender_mesh.loops), 3)
self.normals = np.round(self.normals, NORMALS_ROUNDING_DIGIT) self.normals = np.round(self.normals, NORMALS_ROUNDING_DIGIT)
# Force normalization of normals in case some normals are not (why ?)
PrimitiveCreator.normalize_vecs(self.normals)
self.morph_normals = [] self.morph_normals = []
for key_block in key_blocks: for key_block in key_blocks:

View File

@ -2,12 +2,11 @@
# Copyright 2018-2021 The glTF-Blender-IO authors. # Copyright 2018-2021 The glTF-Blender-IO authors.
import re import re
import os import os
import urllib.parse
from typing import List from typing import List
from ... import get_version_string from ... import get_version_string
from ...io.com import gltf2_io, gltf2_io_extensions from ...io.com import gltf2_io, gltf2_io_extensions
from ...io.com.gltf2_io_path import path_to_uri from ...io.com.gltf2_io_path import path_to_uri, uri_to_path
from ...io.exp import gltf2_io_binary_data, gltf2_io_buffer, gltf2_io_image_data from ...io.exp import gltf2_io_binary_data, gltf2_io_buffer, gltf2_io_image_data
from ...io.exp.gltf2_io_user_extensions import export_user_extensions from ...io.exp.gltf2_io_user_extensions import export_user_extensions
@ -110,7 +109,7 @@ class GlTF2Exporter:
if is_glb: if is_glb:
uri = None uri = None
elif output_path and buffer_name: elif output_path and buffer_name:
with open(output_path + buffer_name, 'wb') as f: with open(output_path + uri_to_path(buffer_name), 'wb') as f:
f.write(self.__buffer.to_bytes()) f.write(self.__buffer.to_bytes())
uri = buffer_name uri = buffer_name
else: else:

View File

@ -179,6 +179,8 @@ class BlenderGlTF():
# Try to use name from extras.targetNames # Try to use name from extras.targetNames
try: try:
shapekey_name = str(mesh.extras['targetNames'][sk]) shapekey_name = str(mesh.extras['targetNames'][sk])
if shapekey_name == "": # Issue when shapekey name is empty
shapekey_name = None
except Exception: except Exception:
pass pass

View File

@ -46,27 +46,27 @@ class BlenderLight():
sun = bpy.data.lights.new(name=pylight['name'], type="SUN") sun = bpy.data.lights.new(name=pylight['name'], type="SUN")
if 'intensity' in pylight.keys(): if 'intensity' in pylight.keys():
if gltf.import_settings['convert_lighting_mode'] == 'SPEC': if gltf.import_settings['export_import_convert_lighting_mode'] == 'SPEC':
sun.energy = pylight['intensity'] / PBR_WATTS_TO_LUMENS sun.energy = pylight['intensity'] / PBR_WATTS_TO_LUMENS
elif gltf.import_settings['convert_lighting_mode'] == 'COMPAT': elif gltf.import_settings['export_import_convert_lighting_mode'] == 'COMPAT':
sun.energy = pylight['intensity'] sun.energy = pylight['intensity']
elif gltf.import_settings['convert_lighting_mode'] == 'RAW': elif gltf.import_settings['export_import_convert_lighting_mode'] == 'RAW':
sun.energy = pylight['intensity'] sun.energy = pylight['intensity']
else: else:
raise ValueError(gltf.import_settings['convert_lighting_mode']) raise ValueError(gltf.import_settings['export_import_convert_lighting_mode'])
return sun return sun
@staticmethod @staticmethod
def _calc_energy_pointlike(gltf, pylight): def _calc_energy_pointlike(gltf, pylight):
if gltf.import_settings['convert_lighting_mode'] == 'SPEC': if gltf.import_settings['export_import_convert_lighting_mode'] == 'SPEC':
return pylight['intensity'] / PBR_WATTS_TO_LUMENS * 4 * pi return pylight['intensity'] / PBR_WATTS_TO_LUMENS * 4 * pi
elif gltf.import_settings['convert_lighting_mode'] == 'COMPAT': elif gltf.import_settings['export_import_convert_lighting_mode'] == 'COMPAT':
return pylight['intensity'] * 4 * pi return pylight['intensity'] * 4 * pi
elif gltf.import_settings['convert_lighting_mode'] == 'RAW': elif gltf.import_settings['export_import_convert_lighting_mode'] == 'RAW':
return pylight['intensity'] return pylight['intensity']
else: else:
raise ValueError(gltf.import_settings['convert_lighting_mode']) raise ValueError(gltf.import_settings['export_import_convert_lighting_mode'])
@staticmethod @staticmethod
def create_point(gltf, light_id): def create_point(gltf, light_id):

View File

@ -596,7 +596,22 @@ def skin_into_bind_pose(gltf, skin_idx, vert_joints, vert_weights, locs, vert_no
for i in range(4): for i in range(4):
skinning_mats += ws[:, i].reshape(len(ws), 1, 1) * joint_mats[js[:, i]] skinning_mats += ws[:, i].reshape(len(ws), 1, 1) * joint_mats[js[:, i]]
weight_sums += ws[:, i] weight_sums += ws[:, i]
# Normalize weights to one; necessary for old files / quantized weights
# Some invalid files have 0 weight sum.
# To avoid to have this vertices at 0.0 / 0.0 / 0.0
# We set all weight ( aka 1.0 ) to the first bone
zeros_indices = np.where(weight_sums == 0)[0]
if zeros_indices.shape[0] > 0:
print_console('ERROR', 'File is invalid: Some vertices are not assigned to bone(s) ')
vert_weights[0][:, 0][zeros_indices] = 1.0 # Assign to first bone with all weight
# Reprocess IBM for these vertices
skinning_mats[zeros_indices] = np.zeros((4, 4), dtype=np.float32)
for js, ws in zip(vert_joints, vert_weights):
for i in range(4):
skinning_mats[zeros_indices] += ws[:, i][zeros_indices].reshape(len(ws[zeros_indices]), 1, 1) * joint_mats[js[:, i][zeros_indices]]
weight_sums[zeros_indices] += ws[:, i][zeros_indices]
skinning_mats /= weight_sums.reshape(num_verts, 1, 1) skinning_mats /= weight_sums.reshape(num_verts, 1, 1)
skinning_mats_3x3 = skinning_mats[:, :3, :3] skinning_mats_3x3 = skinning_mats[:, :3, :3]

View File

@ -690,11 +690,9 @@ def create_mesh(new_objects,
nbr_vidx = len(face_vert_loc_indices) nbr_vidx = len(face_vert_loc_indices)
faces_loop_start.append(lidx) faces_loop_start.append(lidx)
lidx += nbr_vidx lidx += nbr_vidx
faces_loop_total = tuple(len(face_vert_loc_indices) for (face_vert_loc_indices, _, _, _, _, _, _) in faces)
me.loops.foreach_set("vertex_index", loops_vert_idx) me.loops.foreach_set("vertex_index", loops_vert_idx)
me.polygons.foreach_set("loop_start", faces_loop_start) me.polygons.foreach_set("loop_start", faces_loop_start)
me.polygons.foreach_set("loop_total", faces_loop_total)
faces_ma_index = tuple(material_mapping[context_material] for (_, _, _, context_material, _, _, _) in faces) faces_ma_index = tuple(material_mapping[context_material] for (_, _, _, context_material, _, _, _) in faces)
me.polygons.foreach_set("material_index", faces_ma_index) me.polygons.foreach_set("material_index", faces_ma_index)

View File

@ -1256,7 +1256,7 @@ def gzipOpen(path):
if data is None: if data is None:
try: try:
filehandle = open(path, 'rU', encoding='utf-8', errors='surrogateescape') filehandle = open(path, 'r', encoding='utf-8', errors='surrogateescape')
data = filehandle.read() data = filehandle.read()
filehandle.close() filehandle.close()
except: except:
@ -1720,7 +1720,6 @@ def importMesh_IndexedTriangleSet(geom, ancestry):
bpymesh.loops.add(num_polys * 3) bpymesh.loops.add(num_polys * 3)
bpymesh.polygons.add(num_polys) bpymesh.polygons.add(num_polys)
bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3)) bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3))
bpymesh.polygons.foreach_set("loop_total", (3,) * num_polys)
bpymesh.polygons.foreach_set("vertices", index) bpymesh.polygons.foreach_set("vertices", index)
return importMesh_FinalizeTriangleMesh(bpymesh, geom, ancestry) return importMesh_FinalizeTriangleMesh(bpymesh, geom, ancestry)
@ -1742,7 +1741,6 @@ def importMesh_IndexedTriangleStripSet(geom, ancestry):
bpymesh.loops.add(num_polys * 3) bpymesh.loops.add(num_polys * 3)
bpymesh.polygons.add(num_polys) bpymesh.polygons.add(num_polys)
bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3)) bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3))
bpymesh.polygons.foreach_set("loop_total", (3,) * num_polys)
def triangles(): def triangles():
i = 0 i = 0
@ -1778,7 +1776,6 @@ def importMesh_IndexedTriangleFanSet(geom, ancestry):
bpymesh.loops.add(num_polys * 3) bpymesh.loops.add(num_polys * 3)
bpymesh.polygons.add(num_polys) bpymesh.polygons.add(num_polys)
bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3)) bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3))
bpymesh.polygons.foreach_set("loop_total", (3,) * num_polys)
def triangles(): def triangles():
i = 0 i = 0
@ -1808,7 +1805,6 @@ def importMesh_TriangleSet(geom, ancestry):
bpymesh.loops.add(num_polys * 3) bpymesh.loops.add(num_polys * 3)
bpymesh.polygons.add(num_polys) bpymesh.polygons.add(num_polys)
bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3)) bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3))
bpymesh.polygons.foreach_set("loop_total", (3,) * num_polys)
if ccw: if ccw:
fv = [i for i in range(n)] fv = [i for i in range(n)]
@ -1830,7 +1826,6 @@ def importMesh_TriangleStripSet(geom, ancestry):
bpymesh.loops.add(num_polys * 3) bpymesh.loops.add(num_polys * 3)
bpymesh.polygons.add(num_polys) bpymesh.polygons.add(num_polys)
bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3)) bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3))
bpymesh.polygons.foreach_set("loop_total", (3,) * num_polys)
def triangles(): def triangles():
b = 0 b = 0
@ -1856,7 +1851,6 @@ def importMesh_TriangleFanSet(geom, ancestry):
bpymesh.loops.add(num_polys * 3) bpymesh.loops.add(num_polys * 3)
bpymesh.polygons.add(num_polys) bpymesh.polygons.add(num_polys)
bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3)) bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 3, 3))
bpymesh.polygons.foreach_set("loop_total", (3,) * num_polys)
def triangles(): def triangles():
b = 0 b = 0
@ -2067,7 +2061,6 @@ def importMesh_ElevationGrid(geom, ancestry):
bpymesh.loops.add(num_polys * 4) bpymesh.loops.add(num_polys * 4)
bpymesh.polygons.add(num_polys) bpymesh.polygons.add(num_polys)
bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 4, 4)) bpymesh.polygons.foreach_set("loop_start", range(0, num_polys * 4, 4))
bpymesh.polygons.foreach_set("loop_total", (4,) * num_polys)
# If the ccw is off, we flip the 2nd and the 4th vertices of each face. # If the ccw is off, we flip the 2nd and the 4th vertices of each face.
# For quad tessfaces, it was important that the final vertex index was not 0 # For quad tessfaces, it was important that the final vertex index was not 0
# (Blender treated it as a triangle then). # (Blender treated it as a triangle then).
@ -2481,7 +2474,6 @@ def importMesh_Sphere(geom, ancestry):
tuple(range(0, ns * 3, 3)) + tuple(range(0, ns * 3, 3)) +
tuple(range(ns * 3, num_loop - ns * 3, 4)) + tuple(range(ns * 3, num_loop - ns * 3, 4)) +
tuple(range(num_loop - ns * 3, num_loop, 3))) tuple(range(num_loop - ns * 3, num_loop, 3)))
bpymesh.polygons.foreach_set("loop_total", (3,) * ns + (4,) * num_quad + (3,) * ns)
vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap
fb = (nr - 1) * ns # First face index for the bottom cap fb = (nr - 1) * ns # First face index for the bottom cap

View File

@ -3,8 +3,8 @@
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, 44), "version": (3, 45),
"blender": (3, 4, 0), "blender": (3, 6, 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",
"warning": "", "warning": "",

View File

@ -154,7 +154,8 @@ class NWMergeShadersMenu(Menu, NWBase):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
for type in ('MIX', 'ADD'): for type in ('MIX', 'ADD'):
props = layout.operator(operators.NWMergeNodes.bl_idname, text=type) name = f'{type.capitalize()} Shader'
props = layout.operator(operators.NWMergeNodes.bl_idname, text=name)
props.mode = type props.mode = type
props.merge_type = 'SHADER' props.merge_type = 'SHADER'

View File

@ -1,6 +1,7 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
import bpy import bpy
from bpy_extras.node_utils import connect_sockets
from math import hypot from math import hypot
@ -29,48 +30,42 @@ def node_mid_pt(node, axis):
def autolink(node1, node2, links): def autolink(node1, node2, links):
link_made = False
available_inputs = [inp for inp in node2.inputs if inp.enabled] available_inputs = [inp for inp in node2.inputs if inp.enabled]
available_outputs = [outp for outp in node1.outputs if outp.enabled] available_outputs = [outp for outp in node1.outputs if outp.enabled]
for outp in available_outputs: for outp in available_outputs:
for inp in available_inputs: for inp in available_inputs:
if not inp.is_linked and inp.name == outp.name: if not inp.is_linked and inp.name == outp.name:
link_made = True connect_sockets(outp, inp)
links.new(outp, inp)
return True return True
for outp in available_outputs: for outp in available_outputs:
for inp in available_inputs: for inp in available_inputs:
if not inp.is_linked and inp.type == outp.type: if not inp.is_linked and inp.type == outp.type:
link_made = True connect_sockets(outp, inp)
links.new(outp, inp)
return True return True
# force some connection even if the type doesn't match # force some connection even if the type doesn't match
if available_outputs: if available_outputs:
for inp in available_inputs: for inp in available_inputs:
if not inp.is_linked: if not inp.is_linked:
link_made = True connect_sockets(available_outputs[0], inp)
links.new(available_outputs[0], inp)
return True return True
# even if no sockets are open, force one of matching type # even if no sockets are open, force one of matching type
for outp in available_outputs: for outp in available_outputs:
for inp in available_inputs: for inp in available_inputs:
if inp.type == outp.type: if inp.type == outp.type:
link_made = True connect_sockets(outp, inp)
links.new(outp, inp)
return True return True
# do something! # do something!
for outp in available_outputs: for outp in available_outputs:
for inp in available_inputs: for inp in available_inputs:
link_made = True connect_sockets(outp, inp)
links.new(outp, inp)
return True return True
print("Could not make a link from " + node1.name + " to " + node2.name) print("Could not make a link from " + node1.name + " to " + node2.name)
return link_made return False
def abs_node_location(node): def abs_node_location(node):

View File

@ -3,7 +3,7 @@
bl_info = { bl_info = {
"name": "3D-Print Toolbox", "name": "3D-Print Toolbox",
"author": "Campbell Barton", "author": "Campbell Barton",
"blender": (3, 0, 0), "blender": (3, 6, 0),
"location": "3D View > Sidebar", "location": "3D View > Sidebar",
"description": "Utilities for 3D printing", "description": "Utilities for 3D printing",
"doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/3d_print_toolbox.html", "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/3d_print_toolbox.html",

View File

@ -79,7 +79,8 @@ def write_mesh(context, report_cb):
name = data_("untitled") name = data_("untitled")
# add object name # add object name
name += f"-{bpy.path.clean_name(obj.name)}" import re
name += "-" + re.sub(r'[\\/:*?"<>|]', "", obj.name)
# first ensure the path is created # first ensure the path is created
if export_path: if export_path:
@ -113,17 +114,16 @@ def write_mesh(context, report_cb):
global_scale=global_scale, global_scale=global_scale,
) )
elif export_format == 'PLY': elif export_format == 'PLY':
addon_ensure("io_mesh_ply")
filepath = bpy.path.ensure_ext(filepath, ".ply") filepath = bpy.path.ensure_ext(filepath, ".ply")
ret = bpy.ops.export_mesh.ply( ret = bpy.ops.wm.ply_export(
filepath=filepath, filepath=filepath,
use_ascii=False, ascii_format=False,
use_mesh_modifiers=True, apply_modifiers=True,
use_selection=True, export_selected_objects=True,
global_scale=global_scale, global_scale=global_scale,
use_normals=export_data_layers, export_normals=export_data_layers,
use_uv_coords=export_data_layers, export_uv=export_data_layers,
use_colors=export_data_layers, export_colors="SRGB" if export_data_layers else "NONE",
) )
elif export_format == 'X3D': elif export_format == 'X3D':
addon_ensure("io_scene_x3d") addon_ensure("io_scene_x3d")

View File

@ -8,7 +8,8 @@ bl_info = {
"location": "3D View", "location": "3D View",
"description": "Distribute object instances on another object.", "description": "Distribute object instances on another object.",
"warning": "", "warning": "",
"doc_url": "", "doc_url": "{BLENDER_MANUAL_URL}/addons/object/scatter_objects.html",
"tracker_url": "https://projects.blender.org/blender/blender-addons/issues",
"support": 'OFFICIAL', "support": 'OFFICIAL',
"category": "Object", "category": "Object",
} }

View File

@ -62,7 +62,7 @@ def pose_library_list_item_context_menu(self: UIList, context: Context) -> None:
list = getattr(context, "ui_list", None) list = getattr(context, "ui_list", None)
if not list or list.bl_idname != "UI_UL_asset_view" or list.list_id != "pose_assets": if not list or list.bl_idname != "UI_UL_asset_view" or list.list_id != "pose_assets":
return False return False
if not context.asset_handle: if not context.active_file:
return False return False
return True return True

View File

@ -5,7 +5,7 @@
import bpy import bpy
from .shading import write_object_material_interior from .shading import write_object_material_interior
def export_meta(file, metas, tab_write, DEF_MAT_NAME): def export_meta(file, metas, material_names_dictionary, tab_write, DEF_MAT_NAME):
"""write all POV blob primitives and Blender Metas to exported file """ """write all POV blob primitives and Blender Metas to exported file """
# TODO - blenders 'motherball' naming is not supported. # TODO - blenders 'motherball' naming is not supported.
@ -221,6 +221,7 @@ def export_meta(file, metas, tab_write, DEF_MAT_NAME):
write_object_material_interior(file, one_material, mob, tab_write) write_object_material_interior(file, one_material, mob, tab_write)
# write_object_material_interior(file, one_material, elems[1]) # write_object_material_interior(file, one_material, elems[1])
tab_write(file, "radiosity{importance %3g}\n" % mob.pov.importance_value) tab_write(file, "radiosity{importance %3g}\n" % mob.pov.importance_value)
tab_write(file, "}\n\n") # End of Metaball block tab_write(file, "}\n\n") # End of Metaball block

View File

@ -554,6 +554,7 @@ def write_pov(filename, scene=None, info_callback=None):
model_meta_topology.export_meta(file, model_meta_topology.export_meta(file,
[m for m in sel if m.type == 'META'], [m for m in sel if m.type == 'META'],
material_names_dictionary,
tab_write, tab_write,
DEF_MAT_NAME,) DEF_MAT_NAME,)

View File

@ -16,8 +16,8 @@
bl_info = { bl_info = {
"name": "Sun Position", "name": "Sun Position",
"author": "Michael Martin, Damien Picard", "author": "Michael Martin, Damien Picard",
"version": (3, 3, 1), "version": (3, 5, 0),
"blender": (3, 0, 0), "blender": (3, 2, 0),
"location": "World > Sun Position", "location": "World > Sun Position",
"description": "Show sun position with objects and/or sky texture", "description": "Show sun position with objects and/or sky texture",
"doc_url": "{BLENDER_MANUAL_URL}/addons/lighting/sun_position.html", "doc_url": "{BLENDER_MANUAL_URL}/addons/lighting/sun_position.html",
@ -41,17 +41,22 @@ from bpy.app.handlers import persistent
register_classes, unregister_classes = bpy.utils.register_classes_factory( register_classes, unregister_classes = bpy.utils.register_classes_factory(
(properties.SunPosProperties, (properties.SunPosProperties,
properties.SunPosAddonPreferences, ui_sun.SUNPOS_OT_AddPreset, properties.SunPosAddonPreferences, ui_sun.SUNPOS_OT_AddPreset,
ui_sun.SUNPOS_MT_Presets, ui_sun.SUNPOS_PT_Panel, ui_sun.SUNPOS_PT_Presets, ui_sun.SUNPOS_PT_Panel,
ui_sun.SUNPOS_PT_Location, ui_sun.SUNPOS_PT_Time, hdr.SUNPOS_OT_ShowHdr)) ui_sun.SUNPOS_PT_Location, ui_sun.SUNPOS_PT_Time, hdr.SUNPOS_OT_ShowHdr))
@persistent @persistent
def sun_scene_handler(scene): def sun_scene_handler(scene):
sun_props = bpy.context.scene.sun_pos_properties sun_props = bpy.context.scene.sun_pos_properties
# Force drawing update
sun_props.show_surface = sun_props.show_surface sun_props.show_surface = sun_props.show_surface
sun_props.show_analemmas = sun_props.show_analemmas sun_props.show_analemmas = sun_props.show_analemmas
sun_props.show_north = sun_props.show_north sun_props.show_north = sun_props.show_north
# Force coordinates update
sun_props.latitude = sun_props.latitude
def register(): def register():
register_classes() register_classes()

View File

@ -6,11 +6,19 @@ import gpu
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from mathutils import Vector from mathutils import Vector
from .sun_calc import calc_surface, calc_analemma
if bpy.app.background: # ignore north line in background mode if bpy.app.background: # ignore north line in background mode
def north_update(self, context): def north_update(self, context):
pass pass
def surface_update(self, context):
pass
def analemmas_update(self, context):
pass
else: else:
# North line
shader_interface = gpu.types.GPUStageInterfaceInfo("my_interface") shader_interface = gpu.types.GPUStageInterfaceInfo("my_interface")
shader_interface.flat('VEC2', "v_StartPos") shader_interface.flat('VEC2', "v_StartPos")
shader_interface.smooth('VEC4', "v_VertPos") shader_interface.smooth('VEC4', "v_VertPos")
@ -54,7 +62,7 @@ else:
del shader_info del shader_info
del shader_interface del shader_interface
def draw_north_callback(): def north_draw():
""" """
Set up the compass needle using the current north offset angle Set up the compass needle using the current north offset angle
less 90 degrees. This forces the unit circle to begin at the less 90 degrees. This forces the unit circle to begin at the
@ -84,8 +92,77 @@ else:
def north_update(self, context): def north_update(self, context):
global _north_handle global _north_handle
if self.show_north and _north_handle is None: sun_props = context.scene.sun_pos_properties
_north_handle = bpy.types.SpaceView3D.draw_handler_add(draw_north_callback, (), 'WINDOW', 'POST_VIEW') addon_prefs = context.preferences.addons[__package__].preferences
if addon_prefs.show_overlays and sun_props.show_north:
_north_handle = bpy.types.SpaceView3D.draw_handler_add(north_draw, (), 'WINDOW', 'POST_VIEW')
elif _north_handle is not None: elif _north_handle is not None:
bpy.types.SpaceView3D.draw_handler_remove(_north_handle, 'WINDOW') bpy.types.SpaceView3D.draw_handler_remove(_north_handle, 'WINDOW')
_north_handle = None _north_handle = None
# Analemmas
def analemmas_draw(batch, shader):
shader.uniform_float("color", (1, 0, 0, 1))
batch.draw(shader)
_analemmas_handle = None
def analemmas_update(self, context):
global _analemmas_handle
sun_props = context.scene.sun_pos_properties
addon_prefs = context.preferences.addons[__package__].preferences
if addon_prefs.show_overlays and sun_props.show_analemmas:
coords = []
indices = []
coord_offset = 0
for h in range(24):
analemma_verts = calc_analemma(context, h)
coords.extend(analemma_verts)
for i in range(len(analemma_verts) - 1):
indices.append((coord_offset + i,
coord_offset + i+1))
coord_offset += len(analemma_verts)
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'LINES',
{"pos": coords}, indices=indices)
if _analemmas_handle is not None:
bpy.types.SpaceView3D.draw_handler_remove(_analemmas_handle, 'WINDOW')
_analemmas_handle = bpy.types.SpaceView3D.draw_handler_add(
analemmas_draw, (batch, shader), 'WINDOW', 'POST_VIEW')
elif _analemmas_handle is not None:
bpy.types.SpaceView3D.draw_handler_remove(_analemmas_handle, 'WINDOW')
_analemmas_handle = None
# Surface
def surface_draw(batch, shader):
blend = gpu.state.blend_get()
gpu.state.blend_set("ALPHA")
shader.uniform_float("color", (.8, .6, 0, 0.2))
batch.draw(shader)
gpu.state.blend_set(blend)
_surface_handle = None
def surface_update(self, context):
global _surface_handle
sun_props = context.scene.sun_pos_properties
addon_prefs = context.preferences.addons[__package__].preferences
if addon_prefs.show_overlays and sun_props.show_surface:
coords = calc_surface(context)
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'TRIS', {"pos": coords})
if _surface_handle is not None:
bpy.types.SpaceView3D.draw_handler_remove(_surface_handle, 'WINDOW')
_surface_handle = bpy.types.SpaceView3D.draw_handler_add(
surface_draw, (batch, shader), 'WINDOW', 'POST_VIEW')
elif _surface_handle is not None:
bpy.types.SpaceView3D.draw_handler_remove(_surface_handle, 'WINDOW')
_surface_handle = None

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# Copyright 2010 Maximilian Hoegner <hp.maxi@hoegners.de>. # Copyright 2010 Maximilian Hoegner <hp.maxi@hoegners.de>.
# geo.py is a python module with no dependencies on extra packages, # geo.py is a python module with no dependencies on extra packages,

View File

@ -95,9 +95,9 @@ def draw_callback_px(self, context):
class SUNPOS_OT_ShowHdr(bpy.types.Operator): class SUNPOS_OT_ShowHdr(bpy.types.Operator):
"""Tooltip""" """Select the location of the Sun in any 3D viewport and keep it in sync with the environment"""
bl_idname = "world.sunpos_show_hdr" bl_idname = "world.sunpos_show_hdr"
bl_label = "Sync Sun to Texture" bl_label = "Pick Sun in Viewport"
exposure: FloatProperty(name="Exposure", default=1.0) exposure: FloatProperty(name="Exposure", default=1.0)
scale: FloatProperty(name="Scale", default=1.0) scale: FloatProperty(name="Scale", default=1.0)
@ -265,7 +265,7 @@ class SUNPOS_OT_ShowHdr(bpy.types.Operator):
nt = context.scene.world.node_tree.nodes nt = context.scene.world.node_tree.nodes
env_tex_node = nt.get(context.scene.sun_pos_properties.hdr_texture) env_tex_node = nt.get(context.scene.sun_pos_properties.hdr_texture)
if env_tex_node.type != "TEX_ENVIRONMENT": if env_tex_node is None or env_tex_node.type != "TEX_ENVIRONMENT":
self.report({'ERROR'}, 'Please select an Environment Texture node') self.report({'ERROR'}, 'Please select an Environment Texture node')
return {'CANCELLED'} return {'CANCELLED'}

View File

@ -4,9 +4,12 @@ import bpy
from bpy.types import AddonPreferences, PropertyGroup from bpy.types import AddonPreferences, PropertyGroup
from bpy.props import (StringProperty, EnumProperty, IntProperty, from bpy.props import (StringProperty, EnumProperty, IntProperty,
FloatProperty, BoolProperty, PointerProperty) FloatProperty, BoolProperty, PointerProperty)
from bpy.app.translations import pgettext_iface as iface_
from .sun_calc import sun_update, parse_coordinates, surface_update, analemmas_update, sun
from .draw import north_update from .draw import north_update, surface_update, analemmas_update
from .geo import parse_position
from .sun_calc import format_lat_long, sun, update_time, move_sun
from math import pi from math import pi
from datetime import datetime from datetime import datetime
@ -16,6 +19,47 @@ TODAY = datetime.today()
# Sun panel properties # Sun panel properties
############################################################################ ############################################################################
parse_success = True
def lat_long_update(self, context):
global parse_success
parse_success = True
sun_update(self, context)
def get_coordinates(self):
if parse_success:
return format_lat_long(self.latitude, self.longitude)
return iface_("ERROR: Could not parse coordinates")
def set_coordinates(self, value):
parsed_co = parse_position(value)
global parse_success
if parsed_co is not None and len(parsed_co) == 2:
latitude, longitude = parsed_co
self.latitude, self.longitude = latitude, longitude
else:
parse_success = False
sun_update(self, bpy.context)
def sun_update(self, context):
sun_props = context.scene.sun_pos_properties
update_time(context)
move_sun(context)
if sun_props.show_surface:
surface_update(self, context)
if sun_props.show_analemmas:
analemmas_update(self, context)
if sun_props.show_north:
north_update(self, context)
class SunPosProperties(PropertyGroup): class SunPosProperties(PropertyGroup):
usage_mode: EnumProperty( usage_mode: EnumProperty(
@ -36,42 +80,49 @@ class SunPosProperties(PropertyGroup):
use_refraction: BoolProperty( use_refraction: BoolProperty(
name="Use Refraction", name="Use Refraction",
description="Show apparent Sun position due to refraction", description="Show the apparent Sun position due to atmospheric refraction",
default=True, default=True,
update=sun_update) update=sun_update)
show_north: BoolProperty( show_north: BoolProperty(
name="Show North", name="Show North",
description="Draw line pointing north", description="Draw a line pointing to the north",
default=False, default=False,
update=north_update) update=north_update)
north_offset: FloatProperty( north_offset: FloatProperty(
name="North Offset", name="North Offset",
description="Rotate the scene to choose North direction", description="Rotate the scene to choose the North direction",
unit="ROTATION", unit="ROTATION",
soft_min=-pi, soft_max=pi, step=10.0, default=0.0, soft_min=-pi, soft_max=pi, step=10.0, default=0.0,
update=sun_update) update=sun_update)
show_surface: BoolProperty( show_surface: BoolProperty(
name="Show Surface", name="Show Surface",
description="Draw sun surface", description="Draw the surface that the Sun occupies in the sky",
default=False, default=False,
update=surface_update) update=surface_update)
show_analemmas: BoolProperty( show_analemmas: BoolProperty(
name="Show Analemmas", name="Show Analemmas",
description="Draw sun analemmas", description="Draw Sun analemmas. These help visualize the motion of the Sun in the sky during the year, for each hour of the day",
default=False, default=False,
update=analemmas_update) update=analemmas_update)
coordinates: StringProperty(
name="Coordinates",
description="Enter coordinates from an online map",
get=get_coordinates,
set=set_coordinates,
options={'SKIP_SAVE'})
latitude: FloatProperty( latitude: FloatProperty(
name="Latitude", name="Latitude",
description="Latitude: (+) Northern (-) Southern", description="Latitude: (+) Northern (-) Southern",
soft_min=-90.0, soft_max=90.0, soft_min=-90.0, soft_max=90.0,
step=5, precision=3, step=5, precision=3,
default=0.0, default=0.0,
update=sun_update) update=lat_long_update)
longitude: FloatProperty( longitude: FloatProperty(
name="Longitude", name="Longitude",
@ -79,40 +130,39 @@ class SunPosProperties(PropertyGroup):
soft_min=-180.0, soft_max=180.0, soft_min=-180.0, soft_max=180.0,
step=5, precision=3, step=5, precision=3,
default=0.0, default=0.0,
update=sun_update) update=lat_long_update)
sunrise_time: FloatProperty( sunrise_time: FloatProperty(
name="Sunrise Time", name="Sunrise Time",
description="Time at which the Sun rises", description="Time at which the Sun rises",
soft_min=0.0, soft_max=24.0, soft_min=0.0, soft_max=24.0,
default=0.0, default=0.0,
get=lambda _: sun.sunrise.time) get=lambda _: sun.sunrise)
sunset_time: FloatProperty( sunset_time: FloatProperty(
name="Sunset Time", name="Sunset Time",
description="Time at which the Sun sets", description="Time at which the Sun sets",
soft_min=0.0, soft_max=24.0, soft_min=0.0, soft_max=24.0,
default=0.0, default=0.0,
get=lambda _: sun.sunset.time) get=lambda _: sun.sunset)
sun_elevation: FloatProperty(
name="Sun Elevation",
description="Elevation angle of the Sun",
soft_min=-pi/2, soft_max=pi/2,
precision=3,
default=0.0,
unit="ROTATION",
get=lambda _: sun.elevation)
sun_azimuth: FloatProperty( sun_azimuth: FloatProperty(
name="Sun Azimuth", name="Sun Azimuth",
description="Rotation angle of the Sun from the north direction", description="Rotation angle of the Sun from the direction of the north",
soft_min=-pi, soft_max=pi, soft_min=-pi, soft_max=pi,
precision=3,
default=0.0, default=0.0,
get=lambda _: sun.azimuth) unit="ROTATION",
get=lambda _: sun.azimuth - bpy.context.scene.sun_pos_properties.north_offset)
sun_elevation: FloatProperty(
name="Sunset Time",
description="Elevation angle of the Sun",
soft_min=-pi/2, soft_max=pi/2,
default=0.0,
get=lambda _: sun.elevation)
co_parser: StringProperty(
name="Enter coordinates",
description="Enter coordinates from an online map",
update=parse_coordinates)
month: IntProperty( month: IntProperty(
name="Month", name="Month",
@ -130,19 +180,19 @@ class SunPosProperties(PropertyGroup):
update=sun_update) update=sun_update)
use_day_of_year: BoolProperty( use_day_of_year: BoolProperty(
description="Use a single value for day of year", description="Use a single value for the day of year",
name="Use day of year", name="Use day of year",
default=False, default=False,
update=sun_update) update=sun_update)
day_of_year: IntProperty( day_of_year: IntProperty(
name="Day of year", name="Day of Year",
min=1, max=366, default=1, min=1, max=366, default=1,
update=sun_update) update=sun_update)
UTC_zone: FloatProperty( UTC_zone: FloatProperty(
name="UTC zone", name="UTC Zone",
description="Time zone: Difference from Greenwich, England in hours", description="Difference from Greenwich, England, in hours",
precision=1, precision=1,
min=-14.0, max=13, step=50, default=0.0, min=-14.0, max=13, step=50, default=0.0,
update=sun_update) update=sun_update)
@ -156,7 +206,7 @@ class SunPosProperties(PropertyGroup):
sun_distance: FloatProperty( sun_distance: FloatProperty(
name="Distance", name="Distance",
description="Distance to sun from origin", description="Distance to the Sun from the origin",
unit="LENGTH", unit="LENGTH",
min=0.0, soft_max=3000.0, step=10.0, default=50.0, min=0.0, soft_max=3000.0, step=10.0, default=50.0,
update=sun_update) update=sun_update)
@ -164,22 +214,22 @@ class SunPosProperties(PropertyGroup):
sun_object: PointerProperty( sun_object: PointerProperty(
name="Sun Object", name="Sun Object",
type=bpy.types.Object, type=bpy.types.Object,
description="Sun object to set in the scene", description="Sun object to use in the scene",
poll=lambda self, obj: obj.type == 'LIGHT', poll=lambda self, obj: obj.type == 'LIGHT',
update=sun_update) update=sun_update)
object_collection: PointerProperty( object_collection: PointerProperty(
name="Collection", name="Collection",
type=bpy.types.Collection, type=bpy.types.Collection,
description="Collection of objects used to visualize sun motion", description="Collection of objects used to visualize the motion of the Sun",
update=sun_update) update=sun_update)
object_collection_type: EnumProperty( object_collection_type: EnumProperty(
name="Display type", name="Display type",
description="Show object collection as sun motion", description="Type of Sun motion to visualize.",
items=( items=(
('ANALEMMA', "Analemma", ""), ('ANALEMMA', "Analemma", "Trajectory of the Sun in the sky during the year, for a given time of the day"),
('DIURNAL', "Diurnal", ""), ('DIURNAL', "Diurnal", "Trajectory of the Sun in the sky during a single day"),
), ),
default='ANALEMMA', default='ANALEMMA',
update=sun_update) update=sun_update)
@ -187,19 +237,19 @@ class SunPosProperties(PropertyGroup):
sky_texture: StringProperty( sky_texture: StringProperty(
name="Sky Texture", name="Sky Texture",
default="", default="",
description="Name of sky texture to be used", description="Name of the sky texture to use",
update=sun_update) update=sun_update)
hdr_texture: StringProperty( hdr_texture: StringProperty(
default="Environment Texture", default="Environment Texture",
name="Environment Texture", name="Environment Texture",
description="Name of texture to use. World nodes must be enabled " description="Name of the environment texture to use. World nodes must be enabled "
"and color set to Environment Texture", "and the color set to an environment Texture",
update=sun_update) update=sun_update)
hdr_azimuth: FloatProperty( hdr_azimuth: FloatProperty(
name="Rotation", name="Rotation",
description="Rotation angle of sun and environment texture", description="Rotation angle of the Sun and environment texture",
unit="ROTATION", unit="ROTATION",
step=10.0, step=10.0,
default=0.0, precision=3, default=0.0, precision=3,
@ -207,7 +257,7 @@ class SunPosProperties(PropertyGroup):
hdr_elevation: FloatProperty( hdr_elevation: FloatProperty(
name="Elevation", name="Elevation",
description="Elevation angle of sun", description="Elevation angle of the Sun",
unit="ROTATION", unit="ROTATION",
step=10.0, step=10.0,
default=0.0, precision=3, default=0.0, precision=3,
@ -215,13 +265,13 @@ class SunPosProperties(PropertyGroup):
bind_to_sun: BoolProperty( bind_to_sun: BoolProperty(
name="Bind Texture to Sun", name="Bind Texture to Sun",
description="If true, Environment texture moves with sun", description="If enabled, the environment texture moves with the Sun",
default=False, default=False,
update=sun_update) update=sun_update)
time_spread: FloatProperty( time_spread: FloatProperty(
name="Time Spread", name="Time Spread",
description="Time period in which to spread object collection", description="Time period around which to spread object collection",
precision=4, precision=4,
soft_min=1.0, soft_max=24.0, step=1.0, default=23.0, soft_min=1.0, soft_max=24.0, step=1.0, default=23.0,
update=sun_update) update=sun_update)
@ -234,53 +284,24 @@ class SunPosProperties(PropertyGroup):
class SunPosAddonPreferences(AddonPreferences): class SunPosAddonPreferences(AddonPreferences):
bl_idname = __package__ bl_idname = __package__
show_time_place: BoolProperty( show_overlays: BoolProperty(
name="Time and place presets", name="Show Overlays",
description="Show time/place presets", description="Display overlays in the viewport: the direction of the north, analemmas and the Sun surface",
default=False)
show_dms: BoolProperty(
name="D° M' S\"",
description="Show lat/long degrees, minutes, seconds labels",
default=True)
show_north: BoolProperty(
name="Show North",
description="Show north offset choice and slider",
default=True,
update=sun_update)
show_surface: BoolProperty(
name="Show Surface",
description="Show sun surface choice and slider",
default=True,
update=sun_update)
show_analemmas: BoolProperty(
name="Show Analemmas",
description="Show analemmas choice and slider",
default=True, default=True,
update=sun_update) update=sun_update)
show_refraction: BoolProperty( show_refraction: BoolProperty(
name="Refraction", name="Refraction",
description="Show sun refraction choice", description="Show Sun Refraction choice",
default=True, default=True)
update=sun_update)
show_az_el: BoolProperty( show_az_el: BoolProperty(
name="Azimuth and elevation info", name="Azimuth and Elevation Info",
description="Show azimuth and solar elevation info", description="Show azimuth and solar elevation info",
default=True) default=True)
show_daylight_savings: BoolProperty(
name="Daylight savings",
description="Show daylight savings time choice",
default=True,
update=sun_update)
show_rise_set: BoolProperty( show_rise_set: BoolProperty(
name="Sunrise and sunset info", name="Sunrise and Sunset Info",
description="Show sunrise and sunset labels", description="Show sunrise and sunset labels",
default=True) default=True)
@ -292,12 +313,7 @@ class SunPosAddonPreferences(AddonPreferences):
col.label(text="Show options or labels:") col.label(text="Show options or labels:")
flow = col.grid_flow(columns=0, even_columns=True, even_rows=False, align=False) flow = col.grid_flow(columns=0, even_columns=True, even_rows=False, align=False)
flow.prop(self, "show_time_place")
flow.prop(self, "show_dms")
flow.prop(self, "show_north")
flow.prop(self, "show_surface")
flow.prop(self, "show_analemmas")
flow.prop(self, "show_refraction") flow.prop(self, "show_refraction")
flow.prop(self, "show_overlays")
flow.prop(self, "show_az_el") flow.prop(self, "show_az_el")
flow.prop(self, "show_daylight_savings")
flow.prop(self, "show_rise_set") flow.prop(self, "show_rise_set")

View File

@ -2,6 +2,7 @@
import bpy import bpy
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
import gpu import gpu
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
@ -9,28 +10,20 @@ from mathutils import Euler, Vector
from math import degrees, radians, pi, sin, cos, asin, acos, tan, floor from math import degrees, radians, pi, sin, cos, asin, acos, tan, floor
import datetime import datetime
from .geo import parse_position
class SunInfo: class SunInfo:
""" """
Store intermediate sun calculations Store intermediate sun calculations
""" """
class TAzEl:
time = 0.0
azimuth = 0.0
elevation = 0.0
class CLAMP: class SunBind:
azimuth = 0.0 azimuth = 0.0
elevation = 0.0 elevation = 0.0
az_start_sun = 0.0 az_start_sun = 0.0
az_start_env = 0.0 az_start_env = 0.0
sunrise = TAzEl() bind = SunBind()
sunset = TAzEl()
bind = CLAMP()
bind_to_sun = False bind_to_sun = False
latitude = 0.0 latitude = 0.0
@ -38,6 +31,9 @@ class SunInfo:
elevation = 0.0 elevation = 0.0
azimuth = 0.0 azimuth = 0.0
sunrise = 0.0
sunset = 0.0
month = 0 month = 0
day = 0 day = 0
year = 0 year = 0
@ -52,32 +48,6 @@ class SunInfo:
sun = SunInfo() sun = SunInfo()
def sun_update(self, context):
update_time(context)
move_sun(context)
if self.show_surface:
surface_update(self, context)
if self.show_analemmas:
analemmas_update(self, context)
def parse_coordinates(self, context):
error_message = "ERROR: Could not parse coordinates"
sun_props = context.scene.sun_pos_properties
if sun_props.co_parser:
parsed_co = parse_position(sun_props.co_parser)
if parsed_co is not None and len(parsed_co) == 2:
sun_props.latitude, sun_props.longitude = parsed_co
elif sun_props.co_parser != error_message:
sun_props.co_parser = error_message
# Clear prop
if sun_props.co_parser not in {'', error_message}:
sun_props.co_parser = ''
def move_sun(context): def move_sun(context):
""" """
Cycle through all the selected objects and set their position and rotation Cycle through all the selected objects and set their position and rotation
@ -86,8 +56,6 @@ def move_sun(context):
addon_prefs = context.preferences.addons[__package__].preferences addon_prefs = context.preferences.addons[__package__].preferences
sun_props = context.scene.sun_pos_properties sun_props = context.scene.sun_pos_properties
north_offset = sun_props.north_offset
if sun_props.usage_mode == "HDR": if sun_props.usage_mode == "HDR":
nt = context.scene.world.node_tree.nodes nt = context.scene.world.node_tree.nodes
env_tex = nt.get(sun_props.hdr_texture) env_tex = nt.get(sun_props.hdr_texture)
@ -106,8 +74,7 @@ def move_sun(context):
if sun_props.sun_object: if sun_props.sun_object:
obj = sun_props.sun_object obj = sun_props.sun_object
obj.location = get_sun_vector( obj.location = get_sun_vector(
sun_props.hdr_azimuth, sun_props.hdr_elevation, sun_props.hdr_azimuth, sun_props.hdr_elevation) * sun_props.sun_distance
north_offset) * sun_props.sun_distance
rotation_euler = Euler((sun_props.hdr_elevation - pi/2, rotation_euler = Euler((sun_props.hdr_elevation - pi/2,
0, -sun_props.hdr_azimuth)) 0, -sun_props.hdr_azimuth))
@ -127,16 +94,17 @@ def move_sun(context):
azimuth, elevation = get_sun_coordinates( azimuth, elevation = get_sun_coordinates(
local_time, sun_props.latitude, sun_props.longitude, local_time, sun_props.latitude, sun_props.longitude,
zone, sun_props.month, sun_props.day, sun_props.year, zone, sun_props.month, sun_props.day, sun_props.year)
sun_props.sun_distance)
sun.azimuth = azimuth sun.azimuth = azimuth
sun.elevation = elevation sun.elevation = elevation
sun_vector = get_sun_vector(azimuth, elevation)
if sun_props.sky_texture: if sun_props.sky_texture:
sky_node = bpy.context.scene.world.node_tree.nodes.get(sun_props.sky_texture) sky_node = bpy.context.scene.world.node_tree.nodes.get(sun_props.sky_texture)
if sky_node is not None and sky_node.type == "TEX_SKY": if sky_node is not None and sky_node.type == "TEX_SKY":
sky_node.texture_mapping.rotation.z = 0.0 sky_node.texture_mapping.rotation.z = 0.0
sky_node.sun_direction = get_sun_vector(azimuth, elevation, north_offset) sky_node.sun_direction = sun_vector
sky_node.sun_elevation = elevation sky_node.sun_elevation = elevation
sky_node.sun_rotation = azimuth sky_node.sun_rotation = azimuth
@ -144,7 +112,7 @@ def move_sun(context):
if (sun_props.sun_object is not None if (sun_props.sun_object is not None
and sun_props.sun_object.name in context.view_layer.objects): and sun_props.sun_object.name in context.view_layer.objects):
obj = sun_props.sun_object obj = sun_props.sun_object
obj.location = get_sun_vector(azimuth, elevation, north_offset) * sun_props.sun_distance obj.location = sun_vector * sun_props.sun_distance
rotation_euler = Euler((elevation - pi/2, 0, -azimuth)) rotation_euler = Euler((elevation - pi/2, 0, -azimuth))
set_sun_rotations(obj, rotation_euler) set_sun_rotations(obj, rotation_euler)
@ -164,9 +132,8 @@ def move_sun(context):
azimuth, elevation = get_sun_coordinates( azimuth, elevation = get_sun_coordinates(
local_time, sun_props.latitude, local_time, sun_props.latitude,
sun_props.longitude, zone, sun_props.longitude, zone,
sun_props.month, sun_props.day, sun_props.month, sun_props.day)
sun_props.year, sun_props.sun_distance) obj.location = get_sun_vector(azimuth, elevation) * sun_props.sun_distance
obj.location = get_sun_vector(azimuth, elevation, north_offset) * sun_props.sun_distance
local_time -= time_increment local_time -= time_increment
obj.rotation_euler = ((elevation - pi/2, 0, -azimuth)) obj.rotation_euler = ((elevation - pi/2, 0, -azimuth))
else: else:
@ -179,9 +146,8 @@ def move_sun(context):
azimuth, elevation = get_sun_coordinates( azimuth, elevation = get_sun_coordinates(
local_time, sun_props.latitude, local_time, sun_props.latitude,
sun_props.longitude, zone, sun_props.longitude, zone,
dt.month, dt.day, sun_props.year, dt.month, dt.day, sun_props.year)
sun_props.sun_distance) obj.location = get_sun_vector(azimuth, elevation) * sun_props.sun_distance
obj.location = get_sun_vector(azimuth, elevation, north_offset) * sun_props.sun_distance
day -= day_increment day -= day_increment
obj.rotation_euler = (elevation - pi/2, 0, -azimuth) obj.rotation_euler = (elevation - pi/2, 0, -azimuth)
@ -230,50 +196,46 @@ def sun_handler(scene):
move_sun(bpy.context) move_sun(bpy.context)
def format_time(the_time, daylight_savings, longitude, UTC_zone=None): def format_time(time, daylight_savings, UTC_zone=None):
if UTC_zone is not None: if UTC_zone is not None:
if daylight_savings: if daylight_savings:
UTC_zone += 1 UTC_zone += 1
the_time -= UTC_zone time -= UTC_zone
the_time %= 24 time %= 24
hh = int(the_time) return format_hms(time)
mm = (the_time - int(the_time)) * 60
ss = int((mm - int(mm)) * 60)
return ("%02i:%02i:%02i" % (hh, mm, ss))
def format_hms(the_time): def format_hms(time):
hh = str(int(the_time)) hh = int(time)
min = (the_time - int(the_time)) * 60 mm = (time % 1.0) * 60
sec = int((min - int(min)) * 60) ss = (mm % 1.0) * 60
mm = "0" + str(int(min)) if min < 10 else str(int(min))
ss = "0" + str(sec) if sec < 10 else str(sec)
return (hh + ":" + mm + ":" + ss) return f"{hh:02d}:{int(mm):02d}:{int(ss):02d}"
def format_lat_long(lat_long, is_latitude): def format_lat_long(latitude, longitude):
hh = str(abs(int(lat_long))) coordinates = ""
min = abs((lat_long - int(lat_long)) * 60)
sec = abs(int((min - int(min)) * 60)) for i, co in enumerate((latitude, longitude)):
mm = "0" + str(int(min)) if min < 10 else str(int(min)) dd = abs(int(co))
ss = "0" + str(sec) if sec < 10 else str(sec) mm = abs(co - int(co)) * 60.0
if lat_long == 0: ss = abs(mm - int(mm)) * 60.0
coord_tag = " " if co == 0:
direction = ""
elif i == 0:
direction = "N" if co > 0 else "S"
else: else:
if is_latitude: direction = "E" if co > 0 else "W"
coord_tag = " N" if lat_long > 0 else " S"
else:
coord_tag = " E" if lat_long > 0 else " W"
return hh + "° " + mm + "' " + ss + '"' + coord_tag coordinates += f"{dd:02d}°{int(mm):02d}{ss:05.2f}{direction} "
return coordinates.strip(" ")
def get_sun_coordinates(local_time, latitude, longitude, def get_sun_coordinates(local_time, latitude, longitude,
utc_zone, month, day, year, distance): utc_zone, month, day, year):
""" """
Calculate the actual position of the sun based on input parameters. Calculate the actual position of the sun based on input parameters.
@ -289,7 +251,6 @@ def get_sun_coordinates(local_time, latitude, longitude,
NOAA's web site is: NOAA's web site is:
http://www.esrl.noaa.gov/gmd/grad/solcalc http://www.esrl.noaa.gov/gmd/grad/solcalc
""" """
addon_prefs = bpy.context.preferences.addons[__package__].preferences
sun_props = bpy.context.scene.sun_pos_properties sun_props = bpy.context.scene.sun_pos_properties
longitude *= -1 # for internal calculations longitude *= -1 # for internal calculations
@ -366,14 +327,16 @@ def get_sun_coordinates(local_time, latitude, longitude,
else: else:
elevation = pi/2 - zenith elevation = pi/2 - zenith
azimuth += sun_props.north_offset
return azimuth, elevation return azimuth, elevation
def get_sun_vector(azimuth, elevation, north_offset): def get_sun_vector(azimuth, elevation):
""" """
Convert the sun coordinates to cartesian Convert the sun coordinates to cartesian
""" """
phi = -(azimuth + north_offset) phi = -azimuth
theta = pi/2 - elevation theta = pi/2 - elevation
loc_x = sin(phi) * sin(-theta) loc_x = sin(phi) * sin(-theta)
@ -449,22 +412,14 @@ def calc_sunrise_sunset(rise):
sun.latitude, sun.longitude) sun.latitude, sun.longitude)
time_local = new_time_UTC + (-zone * 60.0) time_local = new_time_UTC + (-zone * 60.0)
tl = time_local / 60.0 tl = time_local / 60.0
azimuth, elevation = get_sun_coordinates(
tl, sun.latitude, sun.longitude,
zone, sun.month, sun.day, sun.year,
sun.sun_distance)
if sun.use_daylight_savings: if sun.use_daylight_savings:
time_local += 60.0 time_local += 60.0
tl = time_local / 60.0 tl = time_local / 60.0
tl %= 24.0 tl %= 24.0
if rise: if rise:
sun.sunrise.time = tl sun.sunrise = tl
sun.sunrise.azimuth = azimuth
sun.sunrise.elevation = elevation
else: else:
sun.sunset.time = tl sun.sunset = tl
sun.sunset.azimuth = azimuth
sun.sunset.elevation = elevation
def julian_time_from_y2k(utc_time, year, month, day): def julian_time_from_y2k(utc_time, year, month, day):
@ -566,13 +521,12 @@ def calc_surface(context):
coords = [] coords = []
sun_props = context.scene.sun_pos_properties sun_props = context.scene.sun_pos_properties
zone = -sun_props.UTC_zone zone = -sun_props.UTC_zone
north_offset = sun_props.north_offset
def get_surface_coordinates(time, month): def get_surface_coordinates(time, month):
azimuth, elevation = get_sun_coordinates( azimuth, elevation = get_sun_coordinates(
time, sun_props.latitude, sun_props.longitude, time, sun_props.latitude, sun_props.longitude,
zone, month, 1, sun_props.year, sun_props.sun_distance) zone, month, 1, sun_props.year)
sun_vector = get_sun_vector(azimuth, elevation, north_offset) * sun_props.sun_distance sun_vector = get_sun_vector(azimuth, elevation) * sun_props.sun_distance
sun_vector.z = max(0, sun_vector.z) sun_vector.z = max(0, sun_vector.z)
return sun_vector return sun_vector
@ -592,76 +546,12 @@ def calc_analemma(context, h):
vertices = [] vertices = []
sun_props = context.scene.sun_pos_properties sun_props = context.scene.sun_pos_properties
zone = -sun_props.UTC_zone zone = -sun_props.UTC_zone
north_offset = sun_props.north_offset
for day_of_year in range(1, 367, 5): for day_of_year in range(1, 367, 5):
day, month = day_of_year_to_month_day(sun_props.year, day_of_year) day, month = day_of_year_to_month_day(sun_props.year, day_of_year)
azimuth, elevation = get_sun_coordinates( azimuth, elevation = get_sun_coordinates(
h, sun_props.latitude, sun_props.longitude, h, sun_props.latitude, sun_props.longitude,
zone, month, day, sun_props.year, zone, month, day, sun_props.year)
sun_props.sun_distance) sun_vector = get_sun_vector(azimuth, elevation) * sun_props.sun_distance
sun_vector = get_sun_vector(azimuth, elevation, north_offset) * sun_props.sun_distance
if sun_vector.z > 0: if sun_vector.z > 0:
vertices.append(sun_vector) vertices.append(sun_vector)
return vertices return vertices
def draw_surface(batch, shader):
blend = gpu.state.blend_get()
gpu.state.blend_set("ALPHA")
shader.uniform_float("color", (.8, .6, 0, 0.2))
batch.draw(shader)
gpu.state.blend_set(blend)
def draw_analemmas(batch, shader):
shader.uniform_float("color", (1, 0, 0, 1))
batch.draw(shader)
_handle_surface = None
def surface_update(self, context):
global _handle_surface
if self.show_surface:
coords = calc_surface(context)
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'TRIS', {"pos": coords})
if _handle_surface is not None:
bpy.types.SpaceView3D.draw_handler_remove(_handle_surface, 'WINDOW')
_handle_surface = bpy.types.SpaceView3D.draw_handler_add(
draw_surface, (batch, shader), 'WINDOW', 'POST_VIEW')
elif _handle_surface is not None:
bpy.types.SpaceView3D.draw_handler_remove(_handle_surface, 'WINDOW')
_handle_surface = None
_handle_analemmas = None
def analemmas_update(self, context):
global _handle_analemmas
if self.show_analemmas:
coords = []
indices = []
coord_offset = 0
for h in range(24):
analemma_verts = calc_analemma(context, h)
coords.extend(analemma_verts)
for i in range(len(analemma_verts) - 1):
indices.append((coord_offset + i,
coord_offset + i+1))
coord_offset += len(analemma_verts)
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'LINES',
{"pos": coords}, indices=indices)
if _handle_analemmas is not None:
bpy.types.SpaceView3D.draw_handler_remove(_handle_analemmas, 'WINDOW')
_handle_analemmas = bpy.types.SpaceView3D.draw_handler_add(
draw_analemmas, (batch, shader), 'WINDOW', 'POST_VIEW')
elif _handle_analemmas is not None:
bpy.types.SpaceView3D.draw_handler_remove(_handle_analemmas, 'WINDOW')
_handle_analemmas = None

View File

@ -10,14 +10,14 @@
translations_tuple = ( translations_tuple = (
(("*", ""), (("*", ""),
((), ()), ((), ()),
("fr_FR", "Project-Id-Version: Sun Position 3.1.2 (0)\n", ("fr_FR", "Project-Id-Version: Sun Position 3.3.3 (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.",
"This file is distributed under the same license as the Blender package.", "This file is distributed under the same license as the Blender package.",
"Damien Picard <dam.pic@free.fr>, 2022."))), "Damien Picard <dam.pic@free.fr>, 2022."))),
), ),
(("*", "Azimuth and elevation info"), (("*", "Azimuth and Elevation Info"),
(("bpy.types.SunPosAddonPreferences.show_az_el",), (("bpy.types.SunPosAddonPreferences.show_az_el",),
()), ()),
("fr_FR", "Infos dazimut et de hauteur", ("fr_FR", "Infos dazimut et de hauteur",
@ -26,60 +26,35 @@ translations_tuple = (
(("*", "Show azimuth and solar elevation info"), (("*", "Show azimuth and solar elevation info"),
(("bpy.types.SunPosAddonPreferences.show_az_el",), (("bpy.types.SunPosAddonPreferences.show_az_el",),
()), ()),
("fr_FR", "Afficher les infos dazimut et de hauteur du soleil", ("fr_FR", "Afficher les infos dazimut et de hauteur du Soleil",
(False, ())), (False, ())),
), ),
(("*", "Daylight savings"), (("*", "Daylight Savings"),
(("bpy.types.SunPosAddonPreferences.show_daylight_savings", (("bpy.types.SunPosProperties.use_daylight_savings"),
"bpy.types.SunPosProperties.use_daylight_savings"),
()), ()),
("fr_FR", "Heure dété", ("fr_FR", "Heure dété",
(False, ())), (False, ())),
), ),
(("*", "Show daylight savings time choice"), (("*", "Display overlays in the viewport: the direction of the north, analemmas and the Sun surface"),
(("bpy.types.SunPosAddonPreferences.show_daylight_savings",), (("bpy.types.SunPosAddonPreferences.show_overlays",),
()), ()),
("fr_FR", "Afficher loption de changement dheure", ("fr_FR", "Afficher des surimpressions dans la vue 3D : la direction du nord, les analemmes et la surface du Soleil",
(False, ())),
),
(("*", "D° M' S\""),
(("bpy.types.SunPosAddonPreferences.show_dms",),
()),
("fr_FR", "",
(False, ())),
),
(("*", "Show lat/long degrees, minutes, seconds labels"),
(("bpy.types.SunPosAddonPreferences.show_dms",),
()),
("fr_FR", "Afficher les étiquettes de latitude et longitude en degrés, minutes, secondes",
(False, ())),
),
(("*", "Show North"),
(("bpy.types.SunPosAddonPreferences.show_north",
"bpy.types.SunPosProperties.show_north"),
()),
("fr_FR", "Afficher le nord",
(False, ())),
),
(("*", "Show north offset choice and slider"),
(("bpy.types.SunPosAddonPreferences.show_north",),
()),
("fr_FR", "Afficher loption et le curseur de décalage du nord",
(False, ())), (False, ())),
), ),
(("*", "Refraction"), (("*", "Refraction"),
(("bpy.types.SunPosAddonPreferences.show_refraction",), (("bpy.types.SunPosAddonPreferences.show_refraction",
"scripts/addons/sun_position/ui_sun.py:151"),
()), ()),
("fr_FR", "Réfraction", ("fr_FR", "Réfraction",
(False, ())), (False, ())),
), ),
(("*", "Show sun refraction choice"), (("*", "Show Sun Refraction choice"),
(("bpy.types.SunPosAddonPreferences.show_refraction",), (("bpy.types.SunPosAddonPreferences.show_refraction",),
()), ()),
("fr_FR", "Afficher loption de réfraction du soleil", ("fr_FR", "Afficher loption de réfraction du Soleil",
(False, ())), (False, ())),
), ),
(("*", "Sunrise and sunset info"), (("*", "Sunrise and Sunset Info"),
(("bpy.types.SunPosAddonPreferences.show_rise_set",), (("bpy.types.SunPosAddonPreferences.show_rise_set",),
()), ()),
("fr_FR", "Infos de lever et coucher", ("fr_FR", "Infos de lever et coucher",
@ -88,19 +63,7 @@ translations_tuple = (
(("*", "Show sunrise and sunset labels"), (("*", "Show sunrise and sunset labels"),
(("bpy.types.SunPosAddonPreferences.show_rise_set",), (("bpy.types.SunPosAddonPreferences.show_rise_set",),
()), ()),
("fr_FR", "Afficher les informations de lever et coucher du soleil", ("fr_FR", "Afficher les informations de lever et coucher du Soleil",
(False, ())),
),
(("*", "Time and place presets"),
(("bpy.types.SunPosAddonPreferences.show_time_place",),
()),
("fr_FR", "Préréglages dheure et de lieu",
(False, ())),
),
(("*", "Show time/place presets"),
(("bpy.types.SunPosAddonPreferences.show_time_place",),
()),
("fr_FR", "Afficher les préréglages dheure et de lieu",
(False, ())), (False, ())),
), ),
(("*", "Sun Position"), (("*", "Sun Position"),
@ -114,56 +77,56 @@ translations_tuple = (
(("*", "Sun Position Settings"), (("*", "Sun Position Settings"),
(("bpy.types.Scene.sun_pos_properties",), (("bpy.types.Scene.sun_pos_properties",),
()), ()),
("fr_FR", "Options de Position du Soleil", ("fr_FR", "Options de position du Soleil",
(False, ())), (False, ())),
), ),
(("*", "Sun Position Presets"), (("*", "Sun Position Presets"),
(("bpy.types.SUNPOS_MT_Presets",), (("bpy.types.SUNPOS_PT_Presets",),
()), ()),
("fr_FR", "Préréglages de position du Soleil", ("fr_FR", "Préréglages de position du Soleil",
(False, ())), (False, ())),
), ),
(("Operator", "Sync Sun to Texture"), (("Operator", "Pick Sun in Viewport"),
(("bpy.types.WORLD_OT_sunpos_show_hdr",), (("bpy.types.WORLD_OT_sunpos_show_hdr",),
()), ()),
("fr_FR", "Synchroniser Soleil et texture", ("fr_FR", "Pointer le Soleil dans la vue",
(False, ())), (False, ())),
), ),
(("*", "UTC zone"), (("*", "Select the location of the Sun in any 3D viewport and keep it in sync with the environment"),
(("bpy.types.WORLD_OT_sunpos_show_hdr",),
()),
("fr_FR", "Sélectionner la position du Soleil dans nimporte quelle vue 3D, puis la synchroniser avec lenvironnement",
(False, ())),
),
(("*", "UTC Zone"),
(("bpy.types.SunPosProperties.UTC_zone",), (("bpy.types.SunPosProperties.UTC_zone",),
()), ()),
("fr_FR", "Fuseau horaire", ("fr_FR", "Fuseau horaire",
(False, ())), (False, ())),
), ),
(("*", "Time zone: Difference from Greenwich, England in hours"), (("*", "Difference from Greenwich, England, in hours"),
(("bpy.types.SunPosProperties.UTC_zone",), (("bpy.types.SunPosProperties.UTC_zone",),
()), ()),
("fr_FR", "Fuseau horaire : différence avec Greenwich, Angleterre, en heures", ("fr_FR", "Différence avec Greenwich, Angleterre, en heures",
(False, ())), (False, ())),
), ),
(("*", "Bind Texture to Sun"), (("*", "Bind Texture to Sun"),
(("bpy.types.SunPosProperties.bind_to_sun", (("bpy.types.SunPosProperties.bind_to_sun",
"scripts/addons/sun_position/ui_sun.py:119"), "scripts/addons/sun_position/ui_sun.py:103"),
()), ()),
("fr_FR", "Lier la texture au Soleil", ("fr_FR", "Lier la texture au Soleil",
(False, ())), (False, ())),
), ),
(("*", "If true, Environment texture moves with sun"), (("*", "If enabled, the environment texture moves with the Sun"),
(("bpy.types.SunPosProperties.bind_to_sun",), (("bpy.types.SunPosProperties.bind_to_sun",),
()), ()),
("fr_FR", "Si actif, la texture denvironnement tourne avec le Soleil", ("fr_FR", "Si actif, la texture denvironnement tourne avec le Soleil",
(False, ())), (False, ())),
), ),
(("*", "Enter coordinates"),
(("bpy.types.SunPosProperties.co_parser",),
()),
("fr_FR", "Saisir coordonnées",
(False, ())),
),
(("*", "Enter coordinates from an online map"), (("*", "Enter coordinates from an online map"),
(("bpy.types.SunPosProperties.co_parser",), (("bpy.types.SunPosProperties.coordinates",),
()), ()),
("fr_FR", "Saisir des coordonnées depuis une carte", ("fr_FR", "Saisir des coordonnées depuis une carte en ligne",
(False, ())), (False, ())),
), ),
(("*", "Day"), (("*", "Day"),
@ -172,34 +135,36 @@ translations_tuple = (
("fr_FR", "Jour", ("fr_FR", "Jour",
(False, ())), (False, ())),
), ),
(("*", "Day of year"), (("*", "Day of Year"),
(("bpy.types.SunPosProperties.day_of_year",), (("bpy.types.SunPosProperties.day_of_year",),
()), ()),
("fr_FR", "Jour de lannée", ("fr_FR", "Jour de lannée",
(False, ())), (False, ())),
), ),
(("*", "Rotation angle of sun and environment texture"), (("*", "Rotation angle of the Sun and environment texture"),
(("bpy.types.SunPosProperties.hdr_azimuth",), (("bpy.types.SunPosProperties.hdr_azimuth",),
()), ()),
("fr_FR", "Angle de rotation du Soleil et de la texture denvironnement", ("fr_FR", "Angle de rotation du Soleil et de la texture denvironnement",
(False, ())), (False, ())),
), ),
(("*", "Elevation"), (("*", "Elevation"),
(("bpy.types.SunPosProperties.hdr_elevation",), (("bpy.types.SunPosProperties.hdr_elevation",
"scripts/addons/sun_position/ui_sun.py:185"),
()), ()),
("fr_FR", "Hauteur", ("fr_FR", "Hauteur",
(False, ())), (False, ())),
), ),
(("*", "Elevation angle of sun"), (("*", "Elevation angle of the Sun"),
(("bpy.types.SunPosProperties.hdr_elevation",), (("bpy.types.SunPosProperties.hdr_elevation",
"bpy.types.SunPosProperties.sun_elevation"),
()), ()),
("fr_FR", "Angle de hauteur du Soleil", ("fr_FR", "Angle de hauteur du Soleil",
(False, ())), (False, ())),
), ),
(("*", "Name of texture to use. World nodes must be enabled and color set to Environment Texture"), (("*", "Name of the environment texture to use. World nodes must be enabled and the color set to an environment Texture"),
(("bpy.types.SunPosProperties.hdr_texture",), (("bpy.types.SunPosProperties.hdr_texture",),
()), ()),
("fr_FR", "Nom de la texture à utiliser. Les nœuds de shader du monde doivent être activés, et la couleur utiliser une texture denvironnement", ("fr_FR", "Nom de la texture denvironnement à utiliser. Les nœuds de shader du monde doivent être activés, et la couleur utiliser une texture denvironnement",
(False, ())), (False, ())),
), ),
(("*", "Latitude"), (("*", "Latitude"),
@ -233,27 +198,28 @@ translations_tuple = (
(False, ())), (False, ())),
), ),
(("*", "North Offset"), (("*", "North Offset"),
(("bpy.types.SunPosProperties.north_offset",), (("bpy.types.SunPosProperties.north_offset",
"scripts/addons/sun_position/ui_sun.py:181"),
()), ()),
("fr_FR", "Décalage du nord", ("fr_FR", "Décalage du nord",
(False, ())), (False, ())),
), ),
(("*", "Rotate the scene to choose North direction"), (("*", "Rotate the scene to choose the North direction"),
(("bpy.types.SunPosProperties.north_offset",), (("bpy.types.SunPosProperties.north_offset",),
()), ()),
("fr_FR", "Tourner la scène pour choisir la direction du nord", ("fr_FR", "Tourner la scène pour choisir la direction du nord",
(False, ())), (False, ())),
), ),
(("*", "Collection of objects used to visualize sun motion"), (("*", "Collection of objects used to visualize the motion of the Sun"),
(("bpy.types.SunPosProperties.object_collection",), (("bpy.types.SunPosProperties.object_collection",),
()), ()),
("fr_FR", "Collection dobjets utilisée pour visualiser la trajectoire du Soleil", ("fr_FR", "Collection dobjets utilisée pour visualiser la trajectoire du Soleil",
(False, ())), (False, ())),
), ),
(("*", "Show object collection as sun motion"), (("*", "Type of Sun motion to visualize."),
(("bpy.types.SunPosProperties.object_collection_type",), (("bpy.types.SunPosProperties.object_collection_type",),
()), ()),
("fr_FR", "Afficher la collection en tant que", ("fr_FR", "Type de trajectoire du Soleil à visualiser",
(False, ())), (False, ())),
), ),
(("*", "Analemma"), (("*", "Analemma"),
@ -262,41 +228,118 @@ translations_tuple = (
("fr_FR", "Analemme", ("fr_FR", "Analemme",
(False, ())), (False, ())),
), ),
(("*", "Trajectory of the Sun in the sky during the year, for a given time of the day"),
(("bpy.types.SunPosProperties.object_collection_type:'ANALEMMA'",),
()),
("fr_FR", "Trajectoire du Soleil pendant lannée, pour une heure donnée du jour",
(False, ())),
),
(("*", "Diurnal"), (("*", "Diurnal"),
(("bpy.types.SunPosProperties.object_collection_type:'DIURNAL'",), (("bpy.types.SunPosProperties.object_collection_type:'DIURNAL'",),
()), ()),
("fr_FR", "Diurne", ("fr_FR", "Diurne",
(False, ())), (False, ())),
), ),
(("*", "Draw line pointing north"), (("*", "Trajectory of the Sun in the sky during a single day"),
(("bpy.types.SunPosProperties.object_collection_type:'DIURNAL'",),
()),
("fr_FR", "Trajectoire du Soleil pendant un seul jour",
(False, ())),
),
(("*", "Show Analemmas"),
(("bpy.types.SunPosProperties.show_analemmas",),
()),
("fr_FR", "Afficher les analemmes",
(False, ())),
),
(("*", "Draw Sun analemmas. These help visualize the motion of the Sun in the sky during the year, for each hour of the day"),
(("bpy.types.SunPosProperties.show_analemmas",),
()),
("fr_FR", "Afficher les analemmes du soleil. Ils aident à visualiser la trajectoire du Soleil dans le ciel pendant lannée, pour chaque heure du jour",
(False, ())),
),
(("*", "Show North"),
(("bpy.types.SunPosProperties.show_north",),
()),
("fr_FR", "Afficher le nord",
(False, ())),
),
(("*", "Draw a line pointing to the north"),
(("bpy.types.SunPosProperties.show_north",), (("bpy.types.SunPosProperties.show_north",),
()), ()),
("fr_FR", "Afficher une ligne pointant le nord", ("fr_FR", "Afficher une ligne pointant le nord",
(False, ())), (False, ())),
), ),
(("*", "Name of sky texture to be used"), (("*", "Show Surface"),
(("bpy.types.SunPosProperties.sky_texture",), (("bpy.types.SunPosProperties.show_surface",),
()), ()),
("fr_FR", "Nom de la texture à utiliser", ("fr_FR", "Afficher la surface",
(False, ())), (False, ())),
), ),
(("*", "Distance to sun from origin"), (("*", "Draw the surface that the Sun occupies in the sky"),
(("bpy.types.SunPosProperties.show_surface",),
()),
("fr_FR", "Afficher la surface que le Soleil occupe dans le ciel",
(False, ())),
),
(("*", "Name of the sky texture to use"),
(("bpy.types.SunPosProperties.sky_texture",),
()),
("fr_FR", "Nom de la texture de ciel à utiliser",
(False, ())),
),
(("*", "Sun Azimuth"),
(("bpy.types.SunPosProperties.sun_azimuth",),
()),
("fr_FR", "Azimut du Soleil",
(False, ())),
),
(("*", "Rotation angle of the Sun from the direction of the north"),
(("bpy.types.SunPosProperties.sun_azimuth",),
()),
("fr_FR", "Angle de rotation du Soleil depuis la direction du nord",
(False, ())),
),
(("*", "Distance to the Sun from the origin"),
(("bpy.types.SunPosProperties.sun_distance",), (("bpy.types.SunPosProperties.sun_distance",),
()), ()),
("fr_FR", "Distance entre lorigine et le Soleil", ("fr_FR", "Distance entre lorigine et le Soleil",
(False, ())), (False, ())),
), ),
(("*", "Sun Object"), (("*", "Sun Object"),
(("bpy.types.SunPosProperties.sun_object",
"scripts/addons/sun_position/ui_sun.py:101"),
()),
("fr_FR", "Objet soleil",
(False, ())),
),
(("*", "Sun object to set in the scene"),
(("bpy.types.SunPosProperties.sun_object",), (("bpy.types.SunPosProperties.sun_object",),
()), ()),
("fr_FR", "Objet soleil à utiliser dans la scène", ("fr_FR", "Objet Soleil",
(False, ())),
),
(("*", "Sun object to use in the scene"),
(("bpy.types.SunPosProperties.sun_object",),
()),
("fr_FR", "Objet Soleil à utiliser dans la scène",
(False, ())),
),
(("*", "Sunrise Time"),
(("bpy.types.SunPosProperties.sunrise_time",),
()),
("fr_FR", "Heure de lever",
(False, ())),
),
(("*", "Time at which the Sun rises"),
(("bpy.types.SunPosProperties.sunrise_time",),
()),
("fr_FR", "Heure à laquelle le Soleil se lève",
(False, ())),
),
(("*", "Sunset Time"),
(("bpy.types.SunPosProperties.sunset_time",),
()),
("fr_FR", "Heure de coucher",
(False, ())),
),
(("*", "Time at which the Sun sets"),
(("bpy.types.SunPosProperties.sunset_time",),
()),
("fr_FR", "Heure à laquelle le Soleil se couche",
(False, ())), (False, ())),
), ),
(("*", "Time of the day"), (("*", "Time of the day"),
@ -311,16 +354,16 @@ translations_tuple = (
("fr_FR", "Plage horaire", ("fr_FR", "Plage horaire",
(False, ())), (False, ())),
), ),
(("*", "Time period in which to spread object collection"), (("*", "Time period around which to spread object collection"),
(("bpy.types.SunPosProperties.time_spread",), (("bpy.types.SunPosProperties.time_spread",),
()), ()),
("fr_FR", "Plage horaire à visualiser par les objets de la collection", ("fr_FR", "Plage horaire à visualiser par les objets de la collection",
(False, ())), (False, ())),
), ),
(("*", "Usage mode"), (("*", "Usage Mode"),
(("bpy.types.SunPosProperties.usage_mode",), (("bpy.types.SunPosProperties.usage_mode",),
()), ()),
("fr_FR", "Mode", ("fr_FR", "Mode dutilisation",
(False, ())), (False, ())),
), ),
(("*", "Operate in normal mode or environment texture mode"), (("*", "Operate in normal mode or environment texture mode"),
@ -332,7 +375,7 @@ translations_tuple = (
(("*", "Sun + HDR texture"), (("*", "Sun + HDR texture"),
(("bpy.types.SunPosProperties.usage_mode:'HDR'",), (("bpy.types.SunPosProperties.usage_mode:'HDR'",),
()), ()),
("fr_FR", "Soleil + texture HDRI", ("fr_FR", "Soleil et texture HDRI",
(False, ())), (False, ())),
), ),
(("*", "Use day of year"), (("*", "Use day of year"),
@ -341,7 +384,7 @@ translations_tuple = (
("fr_FR", "Utiliser le jour de lannée", ("fr_FR", "Utiliser le jour de lannée",
(False, ())), (False, ())),
), ),
(("*", "Use a single value for day of year"), (("*", "Use a single value for the day of year"),
(("bpy.types.SunPosProperties.use_day_of_year",), (("bpy.types.SunPosProperties.use_day_of_year",),
()), ()),
("fr_FR", "Utiliser une seule valeur pour le jour de lannée", ("fr_FR", "Utiliser une seule valeur pour le jour de lannée",
@ -353,16 +396,16 @@ translations_tuple = (
("fr_FR", "Lheure dété ajoute une heure à lheure standard", ("fr_FR", "Lheure dété ajoute une heure à lheure standard",
(False, ())), (False, ())),
), ),
(("*", "Use refraction"), (("*", "Use Refraction"),
(("bpy.types.SunPosProperties.use_refraction",), (("bpy.types.SunPosProperties.use_refraction",),
()), ()),
("fr_FR", "Utiliser la réfraction", ("fr_FR", "Utiliser la réfraction",
(False, ())), (False, ())),
), ),
(("*", "Show apparent sun position due to refraction"), (("*", "Show the apparent Sun position due to atmospheric refraction"),
(("bpy.types.SunPosProperties.use_refraction",), (("bpy.types.SunPosProperties.use_refraction",),
()), ()),
("fr_FR", "Afficher la position apparente du Soleil due à la réfraction", ("fr_FR", "Afficher la position apparente du Soleil due à la réfraction atmosphérique",
(False, ())), (False, ())),
), ),
(("*", "Year"), (("*", "Year"),
@ -372,99 +415,111 @@ translations_tuple = (
(False, ())), (False, ())),
), ),
(("*", "Could not find 3D View"), (("*", "Could not find 3D View"),
(("scripts/addons/sun_position/hdr.py:262",), (("scripts/addons/sun_position/hdr.py:263",),
()), ()),
("fr_FR", "Impossible de trouver la vue 3D", ("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:268",), (("scripts/addons/sun_position/hdr.py:269",),
()), ()),
("fr_FR", "Veuillez utiliser un nœud de texture denvironnement", ("fr_FR", "Veuillez utiliser un nœud de texture denvironnement",
(False, ())), (False, ())),
), ),
(("*", "Unknown projection"), (("*", "Unknown projection"),
(("scripts/addons/sun_position/hdr.py:180",), (("scripts/addons/sun_position/hdr.py:181",),
()), ()),
("fr_FR", "Projection inconnue", ("fr_FR", "Projection inconnue",
(False, ())), (False, ())),
), ),
(("*", "Show options or labels:"), (("*", "Show options or labels:"),
(("scripts/addons/sun_position/properties.py:242",), (("scripts/addons/sun_position/properties.py:297",),
()), ()),
("fr_FR", "Afficher les options et étiquettes :", ("fr_FR", "Afficher les options et étiquettes :",
(False, ())), (False, ())),
), ),
(("*", "Usage Mode"), (("*", "ERROR: Could not parse coordinates"),
(("scripts/addons/sun_position/ui_sun.py:71",), (("scripts/addons/sun_position/sun_calc.py:54",),
()), ()),
("fr_FR", "Mode", ("fr_FR", "ERREUR : Impossible danalyser les coordonnées",
(False, ())), (False, ())),
), ),
(("*", "Environment Texture"), (("Hour", "Time"),
(("scripts/addons/sun_position/ui_sun.py:85",), (("scripts/addons/sun_position/ui_sun.py:224",),
()), ()),
("fr_FR", "Texture denvironnement", ("fr_FR", "Heure",
(False, ())), (False, ())),
), ),
(("*", "Enter Coordinates"), (("*", "Time Local:"),
(("scripts/addons/sun_position/ui_sun.py:174",), (("scripts/addons/sun_position/ui_sun.py:242",),
()), ()),
("fr_FR", "Saisir coordonnées", ("fr_FR", "Heure locale :",
(False, ())),
),
(("*", "Local:"),
(("scripts/addons/sun_position/ui_sun.py:269",),
()),
("fr_FR", "Locale :",
(False, ())), (False, ())),
), ),
(("*", "UTC:"), (("*", "UTC:"),
(("scripts/addons/sun_position/ui_sun.py:272",), (("scripts/addons/sun_position/ui_sun.py:243",),
()), ()),
("fr_FR", "UTC :", ("fr_FR", "UTC :",
(False, ())), (False, ())),
), ),
(("*", "Please select World in the World panel."), (("*", "Please select World in the World panel."),
(("scripts/addons/sun_position/ui_sun.py:95", (("scripts/addons/sun_position/ui_sun.py:97",
"scripts/addons/sun_position/ui_sun.py:153"), "scripts/addons/sun_position/ui_sun.py:140"),
()), ()),
("fr_FR", "Veuillez sélectionner le monde dans le panneau Monde", ("fr_FR", "Veuillez sélectionner le monde dans le panneau Monde",
(False, ())), (False, ())),
), ),
(("*", "Release binding"), (("*", "Show"),
(("scripts/addons/sun_position/ui_sun.py:116",), (("scripts/addons/sun_position/ui_sun.py:144",),
()), ()),
("fr_FR", "Annuler le lien", ("fr_FR", "Afficher",
(False, ())), (False, ())),
), ),
(("*", "Azimuth:"), (("*", "North"),
(("scripts/addons/sun_position/ui_sun.py:205",), (("scripts/addons/sun_position/ui_sun.py:145",),
()), ()),
("fr_FR", "Azimut :", ("fr_FR", "Nord",
(False, ())), (False, ())),
), ),
(("*", "Elevation:"), (("*", "Analemmas"),
(("scripts/addons/sun_position/ui_sun.py:208",), (("scripts/addons/sun_position/ui_sun.py:146",),
()), ()),
("fr_FR", "Hauteur :", ("fr_FR", "Analemmes",
(False, ())),
),
(("*", "Surface"),
(("scripts/addons/sun_position/ui_sun.py:147",),
()),
("fr_FR", "Surface",
(False, ())),
),
(("*", "Use"),
(("scripts/addons/sun_position/ui_sun.py:150",),
()),
("fr_FR", "Utiliser",
(False, ())),
),
(("*", "Azimuth"),
(("scripts/addons/sun_position/ui_sun.py:186",),
()),
("fr_FR", "Azimut",
(False, ())), (False, ())),
), ),
(("*", "Sunrise:"), (("*", "Sunrise:"),
(("scripts/addons/sun_position/ui_sun.py:284",), (("scripts/addons/sun_position/ui_sun.py:259",),
()), ()),
("fr_FR", "Lever :", ("fr_FR", "Lever :",
(False, ())), (False, ())),
), ),
(("*", "Sunset:"), (("*", "Sunset:"),
(("scripts/addons/sun_position/ui_sun.py:287",), (("scripts/addons/sun_position/ui_sun.py:260",),
()), ()),
("fr_FR", "Coucher :", ("fr_FR", "Coucher :",
(False, ())), (False, ())),
), ),
(("*", "Please activate Use Nodes in the World panel."), (("*", "Please activate Use Nodes in the World panel."),
(("scripts/addons/sun_position/ui_sun.py:92", (("scripts/addons/sun_position/ui_sun.py:94",
"scripts/addons/sun_position/ui_sun.py:150"), "scripts/addons/sun_position/ui_sun.py:137"),
()), ()),
("fr_FR", "Veuillez activer Utiliser nœuds dans le panneau Monde", ("fr_FR", "Veuillez activer Utiliser nœuds dans le panneau Monde",
(False, ())), (False, ())),

View File

@ -3,10 +3,11 @@
import bpy import bpy
from bpy.types import Operator, Menu from bpy.types import Operator, Menu
from bl_operators.presets import AddPresetBase from bl_operators.presets import AddPresetBase
from bl_ui.utils import PresetPanel
import os import os
from math import degrees from math import degrees
from .sun_calc import (format_lat_long, format_time, format_hms, sun) from .sun_calc import format_lat_long, format_time, format_hms, sun
# ------------------------------------------------------------------- # -------------------------------------------------------------------
@ -14,18 +15,18 @@ from .sun_calc import (format_lat_long, format_time, format_hms, sun)
# ------------------------------------------------------------------- # -------------------------------------------------------------------
class SUNPOS_MT_Presets(Menu): class SUNPOS_PT_Presets(PresetPanel, bpy.types.Panel):
bl_label = "Sun Position Presets" bl_label = "Sun Position Presets"
preset_subdir = "operator/sun_position" preset_subdir = "operator/sun_position"
preset_operator = "script.execute_preset" preset_operator = "script.execute_preset"
draw = Menu.draw_preset preset_add_operator = "world.sunpos_add_preset"
class SUNPOS_OT_AddPreset(AddPresetBase, Operator): class SUNPOS_OT_AddPreset(AddPresetBase, Operator):
'''Add Sun Position preset''' '''Add Sun Position preset'''
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_MT_Presets" preset_menu = "SUNPOS_PT_Presets"
# variable used for all preset values # variable used for all preset values
preset_defines = [ preset_defines = [
@ -61,91 +62,33 @@ class SUNPOS_PT_Panel(bpy.types.Panel):
bl_label = "Sun Position" bl_label = "Sun Position"
bl_options = {'DEFAULT_CLOSED'} bl_options = {'DEFAULT_CLOSED'}
def draw_header_preset(self, _context):
SUNPOS_PT_Presets.draw_panel_header(self.layout)
def draw(self, context): def draw(self, context):
sp = context.scene.sun_pos_properties sun_props = context.scene.sun_pos_properties
p = context.preferences.addons[__package__].preferences
layout = self.layout layout = self.layout
self.draw_panel(context, sp, p, layout) layout.use_property_split = True
layout.use_property_decorate = False
def draw_panel(self, context, sp, p, layout): layout.prop(sun_props, "usage_mode", expand=True)
col = self.layout.column(align=True) layout.separator()
col.label(text="Usage Mode")
row = col.row() if sun_props.usage_mode == "HDR":
row.prop(sp, "usage_mode", expand=True) self.draw_environment_mode_panel(context)
col.separator()
if sp.usage_mode == "HDR":
self.draw_environ_mode_panel(context, sp, p, layout)
else: else:
self.draw_normal_mode_panel(context, sp, p, layout) self.draw_normal_mode_panel(context)
def draw_environ_mode_panel(self, context, sp, p, layout): def draw_environment_mode_panel(self, context):
flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, sun_props = context.scene.sun_pos_properties
even_rows=False, align=False) layout = self.layout
col = flow.column(align=True)
col.label(text="Environment Texture")
if context.scene.world is not None:
if context.scene.world.node_tree is not None:
col.prop_search(sp, "hdr_texture",
context.scene.world.node_tree, "nodes", text="")
else:
col.label(text="Please activate Use Nodes in the World panel.",
icon="ERROR")
else:
col.label(text="Please select World in the World panel.",
icon="ERROR")
col.separator()
col = flow.column(align=True)
col.label(text="Sun Object")
col.prop_search(sp, "sun_object",
context.view_layer, "objects", text="")
col.separator()
col = flow.column(align=True)
col.prop(sp, "sun_distance")
if not sp.bind_to_sun:
col.prop(sp, "hdr_elevation")
col.prop(sp, "hdr_azimuth")
col.separator()
col = flow.column(align=True)
if sp.bind_to_sun:
col.prop(sp, "bind_to_sun", toggle=True, icon="CONSTRAINT",
text="Release binding")
else:
col.prop(sp, "bind_to_sun", toggle=True, icon="CONSTRAINT",
text="Bind Texture to Sun")
row = col.row(align=True)
row.enabled = not sp.bind_to_sun
row.operator("world.sunpos_show_hdr", icon='LIGHT_SUN')
def draw_normal_mode_panel(self, context, sp, p, layout):
if p.show_time_place:
row = layout.row(align=True)
row.menu(SUNPOS_MT_Presets.__name__, text=SUNPOS_MT_Presets.bl_label)
row.operator(SUNPOS_OT_AddPreset.bl_idname, text="", icon='ADD')
row.operator(SUNPOS_OT_AddPreset.bl_idname, text="", icon='REMOVE').remove_active = True
col = layout.column(align=True) col = layout.column(align=True)
col.use_property_split = True col.prop_search(sun_props, "sun_object",
col.use_property_decorate = False context.view_layer, "objects")
col.prop(sp, "sun_object")
col.separator()
col.prop(sp, "object_collection")
if sp.object_collection:
col.prop(sp, "object_collection_type")
if sp.object_collection_type == 'DIURNAL':
col.prop(sp, "time_spread")
col.separator()
if context.scene.world is not None: if context.scene.world is not None:
if context.scene.world.node_tree is not None: if context.scene.world.node_tree is not None:
col.prop_search(sp, "sky_texture", col.prop_search(sun_props, "hdr_texture",
context.scene.world.node_tree, "nodes") context.scene.world.node_tree, "nodes")
else: else:
col.label(text="Please activate Use Nodes in the World panel.", col.label(text="Please activate Use Nodes in the World panel.",
@ -154,6 +97,59 @@ class SUNPOS_PT_Panel(bpy.types.Panel):
col.label(text="Please select World in the World panel.", col.label(text="Please select World in the World panel.",
icon="ERROR") icon="ERROR")
layout.use_property_decorate = True
col = layout.column(align=True)
col.prop(sun_props, "bind_to_sun", text="Bind Texture to Sun")
col.prop(sun_props, "hdr_azimuth")
row = col.row(align=True)
row.active = not sun_props.bind_to_sun
row.prop(sun_props, "hdr_elevation")
col.prop(sun_props, "sun_distance")
col.separator()
col = layout.column(align=True)
row = col.row(align=True)
row.enabled = not sun_props.bind_to_sun
row.operator("world.sunpos_show_hdr", icon='LIGHT_SUN')
def draw_normal_mode_panel(self, context):
sun_props = context.scene.sun_pos_properties
addon_prefs = context.preferences.addons[__package__].preferences
layout = self.layout
col = layout.column(align=True)
col.prop(sun_props, "sun_object")
col.separator()
col.prop(sun_props, "object_collection")
if sun_props.object_collection:
col.prop(sun_props, "object_collection_type")
if sun_props.object_collection_type == 'DIURNAL':
col.prop(sun_props, "time_spread")
col.separator()
if context.scene.world is not None:
if context.scene.world.node_tree is not None:
col.prop_search(sun_props, "sky_texture",
context.scene.world.node_tree, "nodes")
else:
col.label(text="Please activate Use Nodes in the World panel.",
icon="ERROR")
else:
col.label(text="Please select World in the World panel.",
icon="ERROR")
if addon_prefs.show_overlays:
col = layout.column(align=True, heading="Show")
col.prop(sun_props, "show_north", text="North")
col.prop(sun_props, "show_analemmas", text="Analemmas")
col.prop(sun_props, "show_surface", text="Surface")
if addon_prefs.show_refraction:
col = layout.column(align=True, heading="Use")
col.prop(sun_props, "use_refraction", text="Refraction")
class SUNPOS_PT_Location(bpy.types.Panel): class SUNPOS_PT_Location(bpy.types.Panel):
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
@ -164,68 +160,34 @@ class SUNPOS_PT_Location(bpy.types.Panel):
@classmethod @classmethod
def poll(self, context): def poll(self, context):
sp = context.scene.sun_pos_properties sun_props = context.scene.sun_pos_properties
return sp.usage_mode != "HDR" return sun_props.usage_mode != "HDR"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
sp = context.scene.sun_pos_properties layout.use_property_split = True
p = context.preferences.addons[__package__].preferences
sun_props = context.scene.sun_pos_properties
addon_prefs = context.preferences.addons[__package__].preferences
col = layout.column(align=True) col = layout.column(align=True)
col.label(text="Enter Coordinates") col.prop(sun_props, "coordinates", icon='URL')
col.prop(sp, "co_parser", text='', icon='URL') col.prop(sun_props, "latitude")
col.prop(sun_props, "longitude")
layout.separator()
flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False)
col = flow.column(align=True)
col.prop(sp, "latitude")
if p.show_dms:
row = col.row()
row.alignment = 'RIGHT'
row.label(text=format_lat_long(sp.latitude, True))
col = flow.column(align=True)
col.prop(sp, "longitude")
if p.show_dms:
row = col.row()
row.alignment = 'RIGHT'
row.label(text=format_lat_long(sp.longitude, False))
col.separator() col.separator()
if p.show_north: col = layout.column(align=True)
col = flow.column(align=True) col.prop(sun_props, "north_offset", text="North Offset")
col.prop(sp, "show_north", toggle=True)
col.prop(sp, "north_offset") if addon_prefs.show_az_el:
col = layout.column(align=True)
col.prop(sun_props, "sun_elevation", text="Elevation")
col.prop(sun_props, "sun_azimuth", text="Azimuth")
col.separator() col.separator()
if p.show_surface or p.show_analemmas: col = layout.column()
col = flow.column(align=True) col.prop(sun_props, "sun_distance")
if p.show_surface:
col.prop(sp, "show_surface", toggle=True)
if p.show_analemmas:
col.prop(sp, "show_analemmas", toggle=True)
col.separator()
if p.show_az_el:
col = flow.column(align=True)
split = col.split(factor=0.4, align=True)
split.label(text="Azimuth:")
split.label(text=str(round(degrees(sun.azimuth), 3)) + "°")
split = col.split(factor=0.4, align=True)
split.label(text="Elevation:")
split.label(text=str(round(degrees(sun.elevation), 3)) + "°")
col.separator()
if p.show_refraction:
col = flow.column()
col.prop(sp, "use_refraction")
col.separator()
col = flow.column()
col.prop(sp, "sun_distance")
col.separator() col.separator()
@ -238,63 +200,67 @@ class SUNPOS_PT_Time(bpy.types.Panel):
@classmethod @classmethod
def poll(self, context): def poll(self, context):
sp = context.scene.sun_pos_properties sun_props = context.scene.sun_pos_properties
return sp.usage_mode != "HDR" return sun_props.usage_mode != "HDR"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
sp = context.scene.sun_pos_properties layout.use_property_split = True
p = context.preferences.addons[__package__].preferences
flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) sun_props = context.scene.sun_pos_properties
addon_prefs = context.preferences.addons[__package__].preferences
col = flow.column(align=True) col = layout.column(align=True)
col.prop(sp, "use_day_of_year", col.prop(sun_props, "use_day_of_year")
icon='SORTTIME') if sun_props.use_day_of_year:
if sp.use_day_of_year: col.prop(sun_props, "day_of_year")
col.prop(sp, "day_of_year")
else: else:
col.prop(sp, "day") col.prop(sun_props, "day")
col.prop(sp, "month") col.prop(sun_props, "month")
col.prop(sp, "year") col.prop(sun_props, "year")
col.separator() col.separator()
col = flow.column(align=True) col = layout.column(align=True)
col.prop(sp, "time") col.prop(sun_props, "time", text="Time", text_ctxt="Hour")
col.prop(sp, "UTC_zone") col.prop(sun_props, "UTC_zone")
if p.show_daylight_savings: col.prop(sun_props, "use_daylight_savings")
col.prop(sp, "use_daylight_savings")
col.separator() col.separator()
col = flow.column(align=True) local_time = format_time(sun_props.time,
lt = format_time(sp.time, sun_props.use_daylight_savings)
p.show_daylight_savings and sp.use_daylight_savings, utc_time = format_time(sun_props.time,
sp.longitude) sun_props.use_daylight_savings,
ut = format_time(sp.time, sun_props.UTC_zone)
p.show_daylight_savings and sp.use_daylight_savings,
sp.longitude, col = layout.column(align=True)
sp.UTC_zone)
col.alignment = 'CENTER' col.alignment = 'CENTER'
split = col.split(factor=0.5, align=True) split = col.split(factor=0.5, align=True)
split.label(text="Local:", icon='TIME') sub = split.column(align=True)
split.label(text=lt) sub.alignment = 'RIGHT'
split = col.split(factor=0.5, align=True) sub.label(text="Time Local:")
split.label(text="UTC:", icon='PREVIEW_RANGE') sub.label(text="UTC:")
split.label(text=ut)
sub = split.column(align=True)
sub.label(text=local_time)
sub.label(text=utc_time)
col.separator() col.separator()
col = flow.column(align=True) if addon_prefs.show_rise_set:
sunrise = format_hms(sun.sunrise)
sunset = format_hms(sun.sunset)
col = layout.column(align=True)
col.alignment = 'CENTER' col.alignment = 'CENTER'
if p.show_rise_set:
sr = format_hms(sun.sunrise.time)
ss = format_hms(sun.sunset.time)
split = col.split(factor=0.5, align=True) split = col.split(factor=0.5, align=True)
split.label(text="Sunrise:", icon='LIGHT_SUN') sub = split.column(align=True)
split.label(text=sr) sub.alignment = 'RIGHT'
split = col.split(factor=0.5, align=True) sub.label(text="Sunrise:")
split.label(text="Sunset:", icon='SOLO_ON') sub.label(text="Sunset:")
split.label(text=ss)
sub = split.column(align=True)
sub.label(text=sunrise)
sub.label(text=sunset)
col.separator() col.separator()

View File

@ -3,8 +3,8 @@
bl_info = { bl_info = {
"name": "Manage UI translations", "name": "Manage UI translations",
"author": "Bastien Montagne", "author": "Bastien Montagne",
"version": (1, 3, 2), "version": (1, 3, 3),
"blender": (2, 92, 0), "blender": (3, 6, 0),
"location": "Main \"File\" menu, text editor, any UI control", "location": "Main \"File\" menu, text editor, any UI control",
"description": "Allows managing UI translations directly from Blender " "description": "Allows managing UI translations directly from Blender "
"(update main .po files, update scripts' translations, etc.)", "(update main .po files, update scripts' translations, etc.)",

View File

@ -141,25 +141,34 @@ class UI_OT_i18n_cleanuptranslation_svn_branches(Operator):
def i18n_updatetranslation_svn_trunk_callback(lng, settings): def i18n_updatetranslation_svn_trunk_callback(lng, settings):
reports = []
if lng['uid'] in settings.IMPORT_LANGUAGES_SKIP: if lng['uid'] in settings.IMPORT_LANGUAGES_SKIP:
print("Skipping {} language ({}), edit settings if you want to enable it.\n".format(lng['name'], lng['uid'])) reports.append("Skipping {} language ({}), edit settings if you want to enable it.".format(lng['name'], lng['uid']))
return lng['uid'], 0.0 return lng['uid'], 0.0, reports
if not lng['use']: if not lng['use']:
print("Skipping {} language ({}).\n".format(lng['name'], lng['uid'])) reports.append("Skipping {} language ({}).".format(lng['name'], lng['uid']))
return lng['uid'], 0.0 return lng['uid'], 0.0, reports
po = utils_i18n.I18nMessages(uid=lng['uid'], kind='PO', src=lng['po_path'], settings=settings) po = utils_i18n.I18nMessages(uid=lng['uid'], kind='PO', src=lng['po_path'], settings=settings)
errs = po.check(fix=True) errs = po.check(fix=True)
print("Processing {} language ({}).\n" reports.append("Processing {} language ({}).\n"
"Cleaned up {} commented messages.\n".format(lng['name'], lng['uid'], po.clean_commented()) + "Cleaned up {} commented messages.\n".format(lng['name'], lng['uid'], po.clean_commented()) +
("Errors in this po, solved as best as possible!\n\t" + "\n\t".join(errs) if errs else "") + "\n") ("Errors in this po, solved as best as possible!\n\t" + "\n\t".join(errs) if errs else ""))
if lng['uid'] in settings.IMPORT_LANGUAGES_RTL: if lng['uid'] in settings.IMPORT_LANGUAGES_RTL:
po.write(kind="PO", dest=lng['po_path_trunk'][:-3] + "_raw.po") po.write(kind="PO", dest=lng['po_path_trunk'][:-3] + "_raw.po")
po.rtl_process() po.rtl_process()
po.write(kind="PO", dest=lng['po_path_trunk']) po.write(kind="PO", dest=lng['po_path_trunk'])
po.write(kind="PO_COMPACT", dest=lng['po_path_git']) po.write(kind="PO_COMPACT", dest=lng['po_path_git'])
po.write(kind="MO", dest=lng['mo_path_trunk']) ret = po.write(kind="MO", dest=lng['mo_path_trunk'])
if (ret.stdout):
reports.append(ret.stdout.decode().rstrip("\n"))
if (ret.stderr):
stderr_str = ret.stderr.decode().rstrip("\n")
if ret.returncode != 0:
reports.append("ERROR: " + stderr_str)
else:
reports.append(stderr_str)
po.update_info() po.update_info()
return lng['uid'], po.nbr_trans_msgs / po.nbr_msgs return lng['uid'], po.nbr_trans_msgs / po.nbr_msgs, reports
class UI_OT_i18n_updatetranslation_svn_trunk(Operator): class UI_OT_i18n_updatetranslation_svn_trunk(Operator):
@ -178,12 +187,13 @@ class UI_OT_i18n_updatetranslation_svn_trunk(Operator):
context.window_manager.progress_update(0) context.window_manager.progress_update(0)
with concurrent.futures.ProcessPoolExecutor() as exctr: with concurrent.futures.ProcessPoolExecutor() as exctr:
num_langs = len(i18n_sett.langs) num_langs = len(i18n_sett.langs)
for progress, (lng_uid, stats_val) in enumerate(exctr.map(i18n_updatetranslation_svn_trunk_callback, for progress, (lng_uid, stats_val, reports) in enumerate(exctr.map(i18n_updatetranslation_svn_trunk_callback,
[dict(lng.items()) for lng in i18n_sett.langs], [dict(lng.items()) for lng in i18n_sett.langs],
(self.settings,) * num_langs, (self.settings,) * num_langs,
chunksize=4)): chunksize=4)):
context.window_manager.progress_update(progress + 1) context.window_manager.progress_update(progress + 1)
stats[lng_uid] = stats_val stats[lng_uid] = stats_val
print("".join(reports) + "\n")
# Copy pot file from branches to trunk. # Copy pot file from branches to trunk.
shutil.copy2(self.settings.FILE_NAME_POT, self.settings.TRUNK_PO_DIR) shutil.copy2(self.settings.FILE_NAME_POT, self.settings.TRUNK_PO_DIR)