Node Wrangler: Improved accuracy on Align Nodes operator #104551
@ -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 l’animation 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",),
|
||||||
()),
|
()),
|
||||||
|
@ -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'}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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": "",
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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",
|
||||||
|
@ -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
164
io_scene_3ds/__init__.py
Normal 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
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
1338
io_scene_3ds/import_3ds.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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.
|
||||||
|
@ -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'):
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
if xform is not None:
|
def blen_read_geom_parse_fbx_data(fbx_data, stride, item_size):
|
||||||
if isinstance(blen_data, list):
|
"""Parse fbx_data as an array.array into a 2d np.ndarray that shares the same memory, where each row is a single
|
||||||
if item_size == 1:
|
item"""
|
||||||
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
|
# Technically stride < item_size could be supported, but there's probably not a use case for it since it would
|
||||||
blen_data[blen_idx] = xform(fbx_data[fbx_idx])
|
# result in a view of the data with self-overlapping memory.
|
||||||
else:
|
assert(stride >= item_size)
|
||||||
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
|
# View the array.array as an np.ndarray.
|
||||||
blen_data[blen_idx] = xform(fbx_data[fbx_idx:fbx_idx + item_size])
|
fbx_data_np = parray_as_ndarray(fbx_data)
|
||||||
else:
|
|
||||||
if item_size == 1:
|
if stride == item_size:
|
||||||
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
|
if item_size > 1:
|
||||||
setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx]))
|
# Need to make sure fbx_data_np has a whole number of items to be able to view item_size elements per row.
|
||||||
else:
|
items_remainder = len(fbx_data_np) % item_size
|
||||||
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
|
if items_remainder:
|
||||||
setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx:fbx_idx + item_size]))
|
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:
|
else:
|
||||||
if isinstance(blen_data, list):
|
# Create a view of fbx_data_np that is only the first item_size elements of each stride. Note that the view will
|
||||||
if item_size == 1:
|
# not be C-contiguous.
|
||||||
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
|
stride_remainder = len(fbx_data_np) % stride
|
||||||
blen_data[blen_idx] = fbx_data[fbx_idx]
|
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:
|
else:
|
||||||
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
|
print("ERROR: not a whole number of strides in this FBX layer! There are a whole number of items, but"
|
||||||
blen_data[blen_idx] = fbx_data[fbx_idx:fbx_idx + item_size]
|
" 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:
|
else:
|
||||||
if item_size == 1:
|
# There's a whole number of strides, so first reshape to one stride per row and then create a view that
|
||||||
def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
|
# includes only the first item_size elements of each stride.
|
||||||
setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx])
|
fbx_data_np = fbx_data_np.reshape(-1, stride)[:, :item_size]
|
||||||
else:
|
|
||||||
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:fbx_idx + item_size])
|
|
||||||
|
|
||||||
for blen_idx, fbx_idx in generator:
|
return fbx_data_np
|
||||||
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_check_fbx_data_length(blen_data, fbx_data_np, is_indices=False):
|
||||||
def blen_read_geom_array_gen_allsame(data_len):
|
"""Check that there are the same number of items in blen_data and fbx_data_np.
|
||||||
return zip(*(range(data_len), (0,) * data_len))
|
|
||||||
|
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_array_gen_direct(fbx_data, stride):
|
def blen_read_geom_xform(fbx_data_np, xform):
|
||||||
fbx_data_len = len(fbx_data)
|
"""xform is either None, or a function that takes fbx_data_np as its only positional argument and returns an
|
||||||
return zip(*(range(fbx_data_len // stride), range(0, fbx_data_len, stride)))
|
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:
|
||||||
|
item_size = fbx_data_np.shape[1]
|
||||||
|
fbx_total_data = fbx_data_np.size
|
||||||
|
fbx_data_np = xform(fbx_data_np)
|
||||||
|
# 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_gen_indextodirect(fbx_layer_index, stride):
|
def blen_read_geom_array_foreach_set_direct(blen_data, blen_attr, blen_dtype, fbx_data, stride, item_size, descr,
|
||||||
return ((bi, fi * stride) for bi, fi in enumerate(fbx_layer_index))
|
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:
|
||||||
|
# Convert the buffer to the Blender C type of blen_attr
|
||||||
|
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:
|
||||||
|
assert(blen_data.size % item_size == 0)
|
||||||
|
blen_data = blen_data.view()
|
||||||
|
blen_data.shape = (-1, item_size)
|
||||||
|
blen_data[:len(fbx_data_np)] = fbx_data_np
|
||||||
|
|
||||||
|
|
||||||
def blen_read_geom_array_gen_direct_looptovert(mesh, fbx_data, stride):
|
def blen_read_geom_array_foreach_set_indexed(blen_data, blen_attr, blen_dtype, fbx_data, fbx_layer_index, stride,
|
||||||
fbx_data_len = len(fbx_data) // stride
|
item_size, descr, xform):
|
||||||
loops = mesh.loops
|
"""Generic fbx_layer to blen_data foreach setter for IndexToDirect layers.
|
||||||
for p in mesh.polygons:
|
blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size.
|
||||||
for lidx in p.loop_indices:
|
fbx_data must be an array.array or a 1d np.ndarray."""
|
||||||
vidx = loops[lidx].vertex_index
|
fbx_data_np = blen_read_geom_parse_fbx_data(fbx_data, stride, item_size)
|
||||||
if vidx < fbx_data_len:
|
fbx_data_np = blen_read_geom_xform(fbx_data_np, xform)
|
||||||
yield lidx, vidx * stride
|
|
||||||
|
# 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:
|
||||||
|
buffer_item_view = blen_data
|
||||||
|
|
||||||
|
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:
|
||||||
|
buffer_item_view[:] = fbx_data_items[fbx_layer_index]
|
||||||
|
|
||||||
|
if blen_data_is_collection:
|
||||||
|
blen_data.foreach_set(blen_attr, buffer.ravel())
|
||||||
|
else:
|
||||||
|
if blen_data_is_collection:
|
||||||
|
# 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:
|
||||||
|
blen_data[:] = fbx_data_items[fbx_layer_index]
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
blen_data[:] = fbx_data_np[0]
|
||||||
|
|
||||||
|
|
||||||
|
def blen_read_geom_array_foreach_set_looptovert(mesh, blen_data, blen_attr, blen_dtype, fbx_data, stride, item_size,
|
||||||
|
descr, xform):
|
||||||
|
"""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"""
|
||||||
|
# The fbx_data is mapped to vertices. To expand fbx_data to polygon loops, get an array of the vertex index of each
|
||||||
|
# polygon loop that will then be used to index fbx_data
|
||||||
|
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,
|
||||||
|
item_size, descr, xform)
|
||||||
|
|
||||||
|
|
||||||
# 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):
|
||||||
elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry')
|
if not fbx_data:
|
||||||
indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'), default=())
|
# No shape key data. Nothing to do.
|
||||||
dvcos = tuple(co for co in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'), default=()))] * 3))
|
return
|
||||||
# We completely ignore normals here!
|
|
||||||
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=()))
|
|
||||||
|
|
||||||
# Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
|
bl_vcos_dtype = np.single
|
||||||
nbr_indices = len(indices)
|
me_vcos = np.empty(len(me.vertices) * 3, dtype=bl_vcos_dtype)
|
||||||
if len(vgweights) == 1 and nbr_indices > 1:
|
me.vertices.foreach_get("co", me_vcos)
|
||||||
vgweights = (vgweights[0],) * nbr_indices
|
me_vcos_vector_view = me_vcos.reshape(-1, 3)
|
||||||
|
|
||||||
assert(len(vgweights) == nbr_indices == len(dvcos))
|
objects = list({node.bl_obj for node in objects})
|
||||||
create_vg = bool(set(vgweights) - {1.0})
|
assert(objects)
|
||||||
|
|
||||||
keyblocks = []
|
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')
|
||||||
|
indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'))
|
||||||
|
dvcos = elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'))
|
||||||
|
|
||||||
for me, objects in meshes:
|
indices = parray_as_ndarray(indices) if indices else np.empty(0, dtype=data_types.ARRAY_INT32)
|
||||||
vcos = tuple((idx, me.vertices[idx].co + Vector(dvco)) for idx, dvco in zip(indices, dvcos))
|
dvcos = parray_as_ndarray(dvcos) if dvcos else np.empty(0, dtype=data_types.ARRAY_FLOAT64)
|
||||||
objects = list({node.bl_obj for node in objects})
|
|
||||||
assert(objects)
|
|
||||||
|
|
||||||
|
# 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!
|
||||||
|
weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0
|
||||||
|
|
||||||
|
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!*
|
||||||
|
nbr_indices = len(indices)
|
||||||
|
if len(vgweights) == 1 and nbr_indices > 1:
|
||||||
|
vgweights = np.full_like(indices, vgweights[0], dtype=vgweights.dtype)
|
||||||
|
|
||||||
|
assert(len(vgweights) == nbr_indices == len(dvcos))
|
||||||
|
|
||||||
|
# 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,20 +3124,29 @@ 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))
|
||||||
# And we have to find all objects using this mesh!
|
# Group shapes by mesh so that each mesh only needs to be processed once for all of its shape
|
||||||
objects = []
|
# keys.
|
||||||
for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
|
if bl_mdata not in mesh_to_shapes:
|
||||||
if o_ctype.props[0] != b'OO':
|
# And we have to find all objects using this mesh!
|
||||||
continue
|
objects = []
|
||||||
node = fbx_helper_nodes[o_uuid]
|
for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
|
||||||
if node:
|
if o_ctype.props[0] != b'OO':
|
||||||
objects.append(node)
|
continue
|
||||||
meshes.append((bl_mdata, objects))
|
node = fbx_helper_nodes[o_uuid]
|
||||||
|
if node:
|
||||||
|
objects.append(node)
|
||||||
|
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):
|
||||||
|
@ -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,24 +450,26 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
export_optimize_animation_size: BoolProperty(
|
export_optimize_animation_size: BoolProperty(
|
||||||
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,
|
||||||
|
@ -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")
|
||||||
|
@ -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:
|
||||||
|
@ -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]
|
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
channels.append(
|
# 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(
|
||||||
|
(
|
||||||
{
|
{
|
||||||
"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
|
||||||
|
@ -53,25 +53,38 @@ def gather_bone_sampled_keyframes(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if not export_settings['gltf_optimize_animation']:
|
if not export_settings['gltf_optimize_animation']:
|
||||||
return keyframes
|
# 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:
|
||||||
# For armatures
|
return keyframes # Always keeping
|
||||||
# Check if all values are the same
|
|
||||||
# In that case, if there is no real keyframe on this channel for this given bone,
|
|
||||||
# We can ignore these keyframes
|
|
||||||
# if there are some fcurve, we can keep only 2 keyframes, first and last
|
|
||||||
cst = fcurve_is_constant(keyframes)
|
|
||||||
|
|
||||||
if node_channel_is_animated is True: # fcurve on this bone for this property
|
|
||||||
# Keep animation, but keep only 2 keyframes if data are not changing
|
|
||||||
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
|
|
||||||
else: # bone is not animated (no fcurve)
|
|
||||||
# Not keeping if not changing property if user decided to not keep
|
|
||||||
if export_settings['gltf_optimize_animation_keep_armature'] is False:
|
|
||||||
return None if cst is True else keyframes
|
|
||||||
else:
|
else:
|
||||||
# Keep at least 2 keyframes if data are not changing
|
# baked bones
|
||||||
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
|
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
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# For armatures
|
||||||
|
# Check if all values are the same
|
||||||
|
# In that case, if there is no real keyframe on this channel for this given bone,
|
||||||
|
# We can ignore these keyframes
|
||||||
|
# if there are some fcurve, we can keep only 2 keyframes, first and last
|
||||||
|
cst = fcurve_is_constant(keyframes)
|
||||||
|
|
||||||
|
if node_channel_is_animated is True: # fcurve on this bone for this property
|
||||||
|
# Keep animation, but keep only 2 keyframes if data are not changing
|
||||||
|
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
|
||||||
|
else: # bone is not animated (no fcurve)
|
||||||
|
# Not keeping if not changing property if user decided to not keep
|
||||||
|
if export_settings['gltf_optimize_animation_keep_armature'] is False:
|
||||||
|
return None if cst is True else keyframes
|
||||||
|
else:
|
||||||
|
# Keep at least 2 keyframes if data are not changing
|
||||||
|
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
|
||||||
|
|
||||||
def fcurve_is_constant(keyframes):
|
def fcurve_is_constant(keyframes):
|
||||||
return all([j < 0.0001 for j in np.ptp([[k.value[i] for i in range(len(keyframes[0].value))] for k in keyframes], axis=0)])
|
return all([j < 0.0001 for j in np.ptp([[k.value[i] for i in range(len(keyframes[0].value))] for k in keyframes], axis=0)])
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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,20 +52,33 @@ def gather_object_sampled_keyframes(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if not export_settings['gltf_optimize_animation']:
|
if not export_settings['gltf_optimize_animation']:
|
||||||
return keyframes
|
# 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:
|
||||||
# For objects, if all values are the same, we keep only first and last
|
return keyframes # Always keeping
|
||||||
cst = fcurve_is_constant(keyframes)
|
|
||||||
if node_channel_is_animated is True:
|
|
||||||
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
|
|
||||||
else:
|
|
||||||
# baked object
|
|
||||||
# Not keeping if not changing property if user decided to not keep
|
|
||||||
if export_settings['gltf_optimize_animation_keep_object'] is False:
|
|
||||||
return None if cst is True else keyframes
|
|
||||||
else:
|
else:
|
||||||
# Keep at least 2 keyframes if data are not changing
|
# 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
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# For objects, if all values are the same, we keep only first and last
|
||||||
|
cst = fcurve_is_constant(keyframes)
|
||||||
|
if node_channel_is_animated is True:
|
||||||
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
|
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
|
||||||
|
else:
|
||||||
|
# baked object
|
||||||
|
# Not keeping if not changing property if user decided to not keep
|
||||||
|
if export_settings['gltf_optimize_animation_keep_object'] is False:
|
||||||
|
return None if cst is True else keyframes
|
||||||
|
else:
|
||||||
|
# Keep at least 2 keyframes if data are not changing
|
||||||
|
return [keyframes[0], keyframes[-1]] if cst is True and len(keyframes) >= 2 else keyframes
|
||||||
|
|
||||||
def fcurve_is_constant(keyframes):
|
def fcurve_is_constant(keyframes):
|
||||||
return all([j < 0.0001 for j in np.ptp([[k.value[i] for i in range(len(keyframes[0].value))] for k in keyframes], axis=0)])
|
return all([j < 0.0001 for j in np.ptp([[k.value[i] for i in range(len(keyframes[0].value))] for k in keyframes], axis=0)])
|
||||||
|
@ -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"
|
||||||
|
@ -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]]:
|
||||||
|
@ -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,26 +207,28 @@ 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
|
||||||
materials = tuple(ms.material for ms in blender_object.material_slots)
|
# modifiers changed them
|
||||||
|
materials = tuple(ms.material for ms in blender_object.material_slots)
|
||||||
|
|
||||||
# retrieve armature
|
# retrieve armature
|
||||||
# Because mesh data will be transforms to skeleton space,
|
# Because mesh data will be transforms to skeleton space,
|
||||||
@ -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
|
||||||
|
@ -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]:
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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]
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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": "",
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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",
|
||||||
|
@ -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")
|
||||||
|
@ -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",
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,7 +221,8 @@ 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
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -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,)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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'}
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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:
|
||||||
else:
|
direction = ""
|
||||||
if is_latitude:
|
elif i == 0:
|
||||||
coord_tag = " N" if lat_long > 0 else " S"
|
direction = "N" if co > 0 else "S"
|
||||||
else:
|
else:
|
||||||
coord_tag = " E" if lat_long > 0 else " W"
|
direction = "E" if co > 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
|
|
||||||
|
@ -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 d’azimut et de hauteur",
|
("fr_FR", "Infos d’azimut 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 d’azimut et de hauteur du soleil",
|
("fr_FR", "Afficher les infos d’azimut 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 l’option de changement d’heure",
|
("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 l’option 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 l’option de réfraction du soleil",
|
("fr_FR", "Afficher l’option 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 d’heure et de lieu",
|
|
||||||
(False, ())),
|
|
||||||
),
|
|
||||||
(("*", "Show time/place presets"),
|
|
||||||
(("bpy.types.SunPosAddonPreferences.show_time_place",),
|
|
||||||
()),
|
|
||||||
("fr_FR", "Afficher les préréglages d’heure 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 n’importe quelle vue 3D, puis la synchroniser avec l’environnement",
|
||||||
|
(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 d’environnement tourne avec le Soleil",
|
("fr_FR", "Si actif, la texture d’environnement 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 l’année",
|
("fr_FR", "Jour de l’anné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 d’environnement",
|
("fr_FR", "Angle de rotation du Soleil et de la texture d’environnement",
|
||||||
(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 d’environnement",
|
("fr_FR", "Nom de la texture d’environnement à utiliser. Les nœuds de shader du monde doivent être activés, et la couleur utiliser une texture d’environnement",
|
||||||
(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 d’objets utilisée pour visualiser la trajectoire du Soleil",
|
("fr_FR", "Collection d’objets 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 l’anné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 l’anné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 l’origine et le Soleil",
|
("fr_FR", "Distance entre l’origine 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 d’utilisation",
|
||||||
(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 l’année",
|
("fr_FR", "Utiliser le jour de l’anné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 l’année",
|
("fr_FR", "Utiliser une seule valeur pour le jour de l’année",
|
||||||
@ -353,16 +396,16 @@ translations_tuple = (
|
|||||||
("fr_FR", "L’heure d’été ajoute une heure à l’heure standard",
|
("fr_FR", "L’heure d’été ajoute une heure à l’heure 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 d’environnement",
|
("fr_FR", "Veuillez utiliser un nœud de texture d’environnement",
|
||||||
(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 d’analyser 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 d’environnement",
|
("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, ())),
|
||||||
|
@ -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:
|
||||||
col.alignment = 'CENTER'
|
sunrise = format_hms(sun.sunrise)
|
||||||
if p.show_rise_set:
|
sunset = format_hms(sun.sunset)
|
||||||
sr = format_hms(sun.sunrise.time)
|
|
||||||
ss = format_hms(sun.sunset.time)
|
col = layout.column(align=True)
|
||||||
|
col.alignment = 'CENTER'
|
||||||
|
|
||||||
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()
|
||||||
|
@ -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.)",
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user