Pose Library: Update to use the asset shelf (when enabled) #104546

Merged
Julian Eisel merged 33 commits from asset-shelf into main 2023-08-04 15:00:21 +02:00
40 changed files with 1488 additions and 1159 deletions
Showing only changes of commit c81ccf751f - Show all commits

View File

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

View File

@ -203,36 +203,36 @@ class BlenderIdPreferences(AddonPreferences):
now = datetime.datetime.utcnow()
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:
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')
else:
time_left = expiry - now
if time_left.days > 14:
exp_str = tip_('on {:%Y-%m-%d}').format(expiry)
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:
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:
exp_str = tip_('in %i minutes.') % round(time_left.seconds / 60)
exp_str = tip_('in %i minutes') % round(time_left.seconds / 60)
else:
exp_str = tip_('within seconds')
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:
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.label(text=msg, icon='WORLD_DATA')
if time_left.days < 14:
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:
col.label(text=tip_('Your authentication token expires %s.') % exp_str,
col.label(text=tip_('Your authentication token expires %s') % exp_str,
icon='BLANK1')
row = layout.row().split(factor=0.8)
@ -307,9 +307,9 @@ class BlenderIdValidate(BlenderIdMixin, Operator):
err = validate_token()
if err is None:
addon_prefs.ok_message = tip_('Authentication token is valid.')
addon_prefs.ok_message = tip_('Authentication token is valid')
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()
@ -329,7 +329,7 @@ class BlenderIdLogout(BlenderIdMixin, Operator):
profiles.logout(BlenderIdProfile.user_id)
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'}

View File

@ -16,7 +16,7 @@ except:
bl_info = {
"name": "Import AutoCAD DXF Format (.dxf)",
"author": "Lukas Treyer, Manfred Moitzi (support + dxfgrabber library), Vladimir Elistratov, Bastien Montagne, Remigiusz Fiedler (AKA migius)",
"version": (0, 9, 6),
"version": (0, 9, 8),
"blender": (2, 80, 0),
"location": "File > Import > AutoCAD DXF",
"description": "Import files in the Autocad DXF format (.dxf)",

View File

@ -9,6 +9,8 @@ __author__ = "mozman <mozman@gmx.at>"
import math
from mathutils import Vector
from . import const
from .color import TrueColor
from .styles import default_text_style
@ -733,11 +735,6 @@ def deg2vec(deg):
return math.cos(rad), math.sin(rad), 0.
def normalized(vector):
x, y, z = vector
m = (x**2 + y**2 + z**2)**0.5
return x/m, y/m, z/m
##################################################
# MTEXT inline codes
# \L Start underline
@ -850,7 +847,7 @@ class MText(DXFEntity):
self.raw_text = "".join(lines)
if xdir is None:
xdir = deg2vec(rotation)
self.xdirection = normalized(xdir)
self.xdirection = Vector(xdir).normalized()
self.set_default_extrusion()
def lines(self):

View File

@ -949,7 +949,7 @@ class Do:
# create the block
if len(block_group.objects) == 0 or name not in self.known_blocks.keys():
bpy.context.screen.scene = block_scene
bpy.context.window.scene = block_scene
block_inserts = [en for en in entity if is_.insert(en.dxftype)]
bc = (en for en in entity if is_.combined_entity(en))
bs = (en for en in entity if is_.separated_entity(en) and not is_.insert(en.dxftype))
@ -985,7 +985,7 @@ class Do:
else:
bbox = self.known_blocks[name][2]
bpy.context.screen.scene = scene
bpy.context.window.scene = scene
o = bbox.copy()
# o.empty_display_size = 0.3
o.instance_type = "COLLECTION"
@ -1379,7 +1379,7 @@ class Do:
return o
def _recenter(self, scene, name):
bpy.context.screen.scene = scene
bpy.context.window.scene = scene
bpy.context.view_layer.update()
bpy.ops.object.select_all(action='DESELECT')
@ -1621,7 +1621,7 @@ class Do:
elif self.pScene is not None: # assume Proj
scene['SRID'] = re.findall(r"\+init=(.+)\s", self.pScene.srs)[0]
#bpy.context.screen.scene = scene
#bpy.context.window.scene = scene
return self.errors
# trying to import dimensions:

View File

@ -16,15 +16,14 @@ import bpy
bl_info = {
"name": "Autodesk 3DS format",
"author": "Bob Holcomb, Campbell Barton, Andreas Atteneder, Sebastian Schrand",
"version": (2, 3, 2),
"version": (2, 3, 4),
"blender": (3, 6, 0),
"location": "File > Import",
"location": "File > Import-Export",
"description": "3DS Import/Export meshes, UVs, materials, textures, "
"cameras & lamps",
"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",
"doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/scene_3ds.html",
"category": "Import-Export",
}
@ -151,14 +150,6 @@ def unregister():
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()

View File

@ -13,19 +13,19 @@ import mathutils
import bpy_extras
from bpy_extras import node_shader_utils
######################################################
# Data Structures
######################################################
###################
# Data Structures #
###################
# Some of the chunks that we will export
# ----- Primary Chunk, at the beginning of each file
# >----- Primary Chunk, at the beginning of each file
PRIMARY = 0x4D4D
# ------ Main Chunks
# >----- Main Chunks
VERSION = 0x0002 # This gives the version of the .3ds file
KFDATA = 0xB000 # This is the header for all of the key frame info
# ------ sub defines of OBJECTINFO
# >----- sub defines of OBJECTINFO
OBJECTINFO = 0x3D3D # Main mesh object chunk before the material and object information
MESHVERSION = 0x3D3E # This gives the version of the mesh
AMBIENTLIGHT = 0x2100 # The color of the ambient light
@ -41,9 +41,16 @@ MATSHINESS = 0xA040 # Specular intensity of the object/material (percent)
MATSHIN2 = 0xA041 # Reflection of the object/material (percent)
MATSHIN3 = 0xA042 # metallic/mirror of the object/material (percent)
MATTRANS = 0xA050 # Transparency value (100-OpacityValue) (percent)
MATSELFILLUM = 0xA080 # # Material self illumination flag
MATSELFILPCT = 0xA084 # Self illumination strength (percent)
MATWIRE = 0xA085 # Material wireframe rendered flag
MATFACEMAP = 0xA088 # Face mapped textures flag
MATPHONGSOFT = 0xA08C # Phong soften material flag
MATWIREABS = 0xA08E # Wire size in units flag
MATWIRESIZE = 0xA087 # Rendered wire size in pixels
MATSHADING = 0xA100 # Material shading method
# >------ sub defines of MAT_MAP
MAT_DIFFUSEMAP = 0xA200 # This is a header for a new diffuse texture
MAT_SPECMAP = 0xA204 # head for specularity map
MAT_OPACMAP = 0xA210 # head for opacity map
@ -53,9 +60,7 @@ MAT_BUMP_PERCENT = 0xA252 # Normalmap strength (percent)
MAT_TEX2MAP = 0xA33A # head for secondary texture
MAT_SHINMAP = 0xA33C # head for roughness map
MAT_SELFIMAP = 0xA33D # head for emission map
# >------ sub defines of MAT_MAP
MATMAPFILE = 0xA300 # This holds the file name of a texture
MAT_MAP_FILE = 0xA300 # This holds the file name of a texture
MAT_MAP_TILING = 0xa351 # 2nd bit (from LSB) is mirror UV flag
MAT_MAP_TEXBLUR = 0xA353 # Texture blurring factor
MAT_MAP_USCALE = 0xA354 # U axis scaling
@ -85,7 +90,10 @@ OBJECT_CAMERA = 0x4700 # This lets us know we are reading a camera object
# >------ Sub defines of LIGHT
LIGHT_MULTIPLIER = 0x465B # The light energy factor
LIGHT_SPOTLIGHT = 0x4610 # The target of a spotlight
LIGHT_SPOTROLL = 0x4656 # The roll angle of the spot
LIGHT_SPOT_ROLL = 0x4656 # Light spot roll angle
LIGHT_SPOT_SHADOWED = 0x4630 # Light spot shadow flag
LIGHT_SPOT_SEE_CONE = 0x4650 # Light spot show cone flag
LIGHT_SPOT_RECTANGLE = 0x4651 # Light spot rectangle flag
# >------ sub defines of CAMERA
OBJECT_CAM_RANGES = 0x4720 # The camera range values
@ -100,19 +108,34 @@ OBJECT_SMOOTH = 0x4150 # The objects smooth groups
OBJECT_TRANS_MATRIX = 0x4160 # The Object Matrix
# >------ sub defines of KFDATA
KFDATA_KFHDR = 0xB00A
KFDATA_KFSEG = 0xB008
KFDATA_KFCURTIME = 0xB009
KFDATA_OBJECT_NODE_TAG = 0xB002
AMBIENT_NODE_TAG = 0xB001 # Ambient node tag
OBJECT_NODE_TAG = 0xB002 # Object tree tag
CAMERA_NODE_TAG = 0xB003 # Camera object tag
TARGET_NODE_TAG = 0xB004 # Camera target tag
LIGHT_NODE_TAG = 0xB005 # Light object tag
LTARGET_NODE_TAG = 0xB006 # Light target tag
SPOT_NODE_TAG = 0xB007 # Spotlight tag
KFDATA_KFSEG = 0xB008 # Frame start & end
KFDATA_KFCURTIME = 0xB009 # Frame current
KFDATA_KFHDR = 0xB00A # Keyframe header
# >------ sub defines of OBJECT_NODE_TAG
OBJECT_NODE_ID = 0xB030
OBJECT_NODE_HDR = 0xB010
OBJECT_PIVOT = 0xB013
OBJECT_INSTANCE_NAME = 0xB011
POS_TRACK_TAG = 0xB020
ROT_TRACK_TAG = 0xB021
SCL_TRACK_TAG = 0xB022
OBJECT_NODE_ID = 0xB030 # Object hierachy ID
OBJECT_NODE_HDR = 0xB010 # Hierachy tree header
OBJECT_INSTANCE_NAME = 0xB011 # Object instance name
OBJECT_PIVOT = 0xB013 # Object pivot position
OBJECT_BOUNDBOX = 0xB014 # Object boundbox
OBJECT_MORPH_SMOOTH = 0xB015 # Object smooth angle
POS_TRACK_TAG = 0xB020 # Position transform tag
ROT_TRACK_TAG = 0xB021 # Rotation transform tag
SCL_TRACK_TAG = 0xB022 # Scale transform tag
FOV_TRACK_TAG = 0xB023 # Field of view tag
ROLL_TRACK_TAG = 0xB024 # Roll transform tag
COL_TRACK_TAG = 0xB025 # Color transform tag
HOTSPOT_TRACK_TAG = 0xB027 # Hotspot transform tag
FALLOFF_TRACK_TAG = 0xB028 # Falloff transform tag
ROOT_OBJECT = 0xFFFF # Root object
# So 3ds max can open files, limit names to 12 in length
@ -120,7 +143,6 @@ SCL_TRACK_TAG = 0xB022
name_unique = [] # stores str, ascii only
name_mapping = {} # stores {orig: byte} mapping
def sane_name(name):
name_fixed = name_mapping.get(name)
if name_fixed is not None:
@ -131,7 +153,7 @@ def sane_name(name):
i = 0
while new_name in name_unique:
new_name = new_name_clean + ".%.3d" % i
new_name = new_name_clean + '.%.3d' % i
i += 1
# note, appending the 'str' version.
@ -143,13 +165,11 @@ def sane_name(name):
def uv_key(uv):
return round(uv[0], 6), round(uv[1], 6)
# size defines:
SZ_SHORT = 2
SZ_INT = 4
SZ_FLOAT = 4
class _3ds_ushort(object):
"""Class representing a short (2-byte integer) for a 3ds file.
*** This looks like an unsigned short H is unsigned from the struct docs - Cam***"""
@ -162,7 +182,7 @@ class _3ds_ushort(object):
return SZ_SHORT
def write(self, file):
file.write(struct.pack("<H", self.value))
file.write(struct.pack('<H', self.value))
def __str__(self):
return str(self.value)
@ -179,7 +199,7 @@ class _3ds_uint(object):
return SZ_INT
def write(self, file):
file.write(struct.pack("<I", self.value))
file.write(struct.pack('<I', self.value))
def __str__(self):
return str(self.value)
@ -196,7 +216,7 @@ class _3ds_float(object):
return SZ_FLOAT
def write(self, file):
file.write(struct.pack("<f", self.value))
file.write(struct.pack('<f', self.value))
def __str__(self):
return str(self.value)
@ -214,7 +234,7 @@ class _3ds_string(object):
return (len(self.value) + 1)
def write(self, file):
binary_format = "<%ds" % (len(self.value) + 1)
binary_format = '<%ds' % (len(self.value) + 1)
file.write(struct.pack(binary_format, self.value))
def __str__(self):
@ -242,19 +262,19 @@ class _3ds_point_3d(object):
'''
class _3ds_point_4d(object):
"""Class representing a four-dimensional point for a 3ds file, for instance a quaternion."""
__slots__ = "x","y","z","w"
__slots__ = "w","x","y","z"
def __init__(self, point=(0.0,0.0,0.0,0.0)):
self.x, self.y, self.z, self.w = point
self.w, self.x, self.y, self.z = point
def get_size(self):
return 4*SZ_FLOAT
def write(self,file):
data=struct.pack('<4f', self.x, self.y, self.z, self.w)
data=struct.pack('<4f', self.w, self.x, self.y, self.z)
file.write(data)
def __str__(self):
return '(%f, %f, %f, %f)' % (self.x, self.y, self.z, self.w)
return '(%f, %f, %f, %f)' % (self.w, self.x, self.y, self.z)
'''
@ -312,7 +332,7 @@ class _3ds_rgb_color(object):
class _3ds_face(object):
"""Class representing a face for a 3ds file."""
__slots__ = ("vindex", "flag")
__slots__ = ("vindex", "flag", )
def __init__(self, vindex, flag):
self.vindex = vindex
@ -326,15 +346,14 @@ class _3ds_face(object):
def write(self, file):
# The last short is used for face flags
file.write(struct.pack("<4H", self.vindex[0], self.vindex[1], self.vindex[2], self.flag))
file.write(struct.pack('<4H', self.vindex[0], self.vindex[1], self.vindex[2], self.flag))
def __str__(self):
return "[%d %d %d %d]" % (self.vindex[0], self.vindex[1], self.vindex[2], self.flag)
return '[%d %d %d %d]' % (self.vindex[0], self.vindex[1], self.vindex[2], self.flag)
class _3ds_array(object):
"""Class representing an array of variables for a 3ds file.
Consists of a _3ds_ushort to indicate the number of items, followed by the items themselves.
"""
__slots__ = "values", "size"
@ -395,7 +414,6 @@ class _3ds_named_variable(object):
# the chunk class
class _3ds_chunk(object):
"""Class representing a chunk in a 3ds file.
Chunks contain zero or more variables, followed by zero or more subchunks.
"""
__slots__ = "ID", "size", "variables", "subchunks"
@ -408,8 +426,8 @@ class _3ds_chunk(object):
def add_variable(self, name, var):
"""Add a named variable.
The name is mostly for debugging purposes."""
self.variables.append(_3ds_named_variable(name, var))
def add_subchunk(self, chunk):
@ -418,8 +436,8 @@ class _3ds_chunk(object):
def get_size(self):
"""Calculate the size of the chunk and return it.
The sizes of the variables and subchunks are used to determine this chunk\'s size."""
tmpsize = self.ID.get_size() + self.size.get_size()
for variable in self.variables:
tmpsize += variable.get_size()
@ -443,8 +461,8 @@ class _3ds_chunk(object):
def write(self, file):
"""Write the chunk to a file.
Uses the write function of the variables and the subchunks to do the actual work."""
# write header
self.ID.write(file)
self.size.write(file)
@ -455,21 +473,20 @@ class _3ds_chunk(object):
def dump(self, indent=0):
"""Write the chunk to a file.
Dump is used for debugging purposes, to dump the contents of a chunk to the standard output.
Uses the dump function of the named variables and the subchunks to do the actual work."""
print(indent * " ",
"ID=%r" % hex(self.ID.value),
"size=%r" % self.get_size())
'ID=%r' % hex(self.ID.value),
'size=%r' % self.get_size())
for variable in self.variables:
variable.dump(indent + 1)
for subchunk in self.subchunks:
subchunk.dump(indent + 1)
######################################################
# EXPORT
######################################################
##########
# EXPORT #
##########
def get_material_image(material):
""" Get images from paint slots."""
@ -485,17 +502,16 @@ def get_material_image(material):
def get_uv_image(ma):
""" Get image from material wrapper."""
if ma and ma.use_nodes:
ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma)
ma_tex = ma_wrap.base_color_texture
if ma_tex and ma_tex.image is not None:
return ma_tex.image
mat_wrap = node_shader_utils.PrincipledBSDFWrapper(ma)
mat_tex = mat_wrap.base_color_texture
if mat_tex and mat_tex.image is not None:
return mat_tex.image
else:
return get_material_image(ma)
def make_material_subchunk(chunk_id, color):
"""Make a material subchunk.
Used for color subchunks, such as diffuse color or ambient color subchunks."""
mat_sub = _3ds_chunk(chunk_id)
col1 = _3ds_chunk(RGB1)
@ -514,26 +530,30 @@ def make_percent_subchunk(chunk_id, percent):
pcti = _3ds_chunk(PCT)
pcti.add_variable("percent", _3ds_ushort(int(round(percent * 100, 0))))
pct_sub.add_subchunk(pcti)
# optional:
# pctf = _3ds_chunk(PCTF)
# pctf.add_variable("pctfloat", _3ds_float(round(percent, 6)))
# pct_sub.add_subchunk(pctf)
return pct_sub
def make_texture_chunk(chunk_id, images):
"""Make Material Map texture chunk."""
# Add texture percentage value (100 = 1.0)
ma_sub = make_percent_subchunk(chunk_id, 1)
mat_sub = make_percent_subchunk(chunk_id, 1)
has_entry = False
def add_image(img):
filename = bpy.path.basename(image.filepath)
ma_sub_file = _3ds_chunk(MATMAPFILE)
ma_sub_file.add_variable("image", _3ds_string(sane_name(filename)))
ma_sub.add_subchunk(ma_sub_file)
mat_sub_file = _3ds_chunk(MAT_MAP_FILE)
mat_sub_file.add_variable("image", _3ds_string(sane_name(filename)))
mat_sub.add_subchunk(mat_sub_file)
for image in images:
add_image(image)
has_entry = True
return ma_sub if has_entry else None
return mat_sub if has_entry else None
def make_material_texture_chunk(chunk_id, texslots, pct):
@ -541,6 +561,7 @@ def make_material_texture_chunk(chunk_id, texslots, pct):
Paint slots are optionally used as image source if no nodes are
used. No additional filtering for mapping modes is done, all
slots are written "as is"."""
# Add texture percentage value
mat_sub = make_percent_subchunk(chunk_id, pct)
has_entry = False
@ -549,35 +570,36 @@ def make_material_texture_chunk(chunk_id, texslots, pct):
image = texslot.image
filename = bpy.path.basename(image.filepath)
mat_sub_file = _3ds_chunk(MATMAPFILE)
mat_sub_file = _3ds_chunk(MAT_MAP_FILE)
mat_sub_file.add_variable("mapfile", _3ds_string(sane_name(filename)))
mat_sub.add_subchunk(mat_sub_file)
for link in texslot.socket_dst.links:
socket = link.from_socket.identifier
maptile = 0
mat_sub_mapflags = _3ds_chunk(MAT_MAP_TILING)
"""Control bit flags, where 0x1 activates decaling, 0x2 activates mirror,
0x8 activates inversion, 0x10 deactivates tiling, 0x20 activates summed area sampling,
0x40 activates alpha source, 0x80 activates tinting, 0x100 ignores alpha, 0x200 activates RGB tint.
Bits 0x80, 0x100, and 0x200 are only used with TEXMAP, TEX2MAP, and SPECMAP chunks.
0x40, when used with a TEXMAP, TEX2MAP, or SPECMAP chunk must be accompanied with a tint bit,
either 0x100 or 0x200, tintcolor will be processed if colorchunks are present"""
mapflags = 0
# no perfect mapping for mirror modes - 3DS only has uniform mirror w. repeat=2
if texslot.extension == 'EXTEND':
maptile |= 0x1
# CLIP maps to 3DS' decal flag
elif texslot.extension == 'CLIP':
maptile |= 0x10
mapflags |= 0x1
mat_sub_tile = _3ds_chunk(MAT_MAP_TILING)
mat_sub_tile.add_variable("tiling", _3ds_ushort(maptile))
mat_sub.add_subchunk(mat_sub_tile)
if texslot.extension == 'CLIP':
mapflags |= 0x10
if socket == 'Alpha':
mat_sub_alpha = _3ds_chunk(MAP_TILING)
alphaflag = 0x40 # summed area sampling 0x20
mat_sub_alpha.add_variable("alpha", _3ds_ushort(alphaflag))
mat_sub.add_subchunk(mat_sub_alpha)
mapflags |= 0x40
if texslot.socket_dst.identifier in {'Base Color', 'Specular'}:
mat_sub_tint = _3ds_chunk(MAP_TILING) # RGB tint 0x200
tint = 0x80 if texslot.image.colorspace_settings.name == 'Non-Color' else 0x200
mat_sub_tint.add_variable("tint", _3ds_ushort(tint))
mat_sub.add_subchunk(mat_sub_tint)
mapflags |= 0x80 if image.colorspace_settings.name=='Non-Color' else 0x200
mat_sub_mapflags.add_variable("mapflags", _3ds_ushort(mapflags))
mat_sub.add_subchunk(mat_sub_mapflags)
mat_sub_texblur = _3ds_chunk(MAT_MAP_TEXBLUR) # Based on observation this is usually 1.0
mat_sub_texblur.add_variable("maptexblur", _3ds_float(1.0))
@ -801,11 +823,11 @@ def extract_triangles(mesh):
faceflag = 0
if c_a.use_edge_sharp:
faceflag = faceflag + 0x1
faceflag |= 0x1
if b_c.use_edge_sharp:
faceflag = faceflag + 0x2
faceflag |= 0x2
if a_b.use_edge_sharp:
faceflag = faceflag + 0x4
faceflag |= 0x4
smoothgroup = polygroup[face.polygon_index]
@ -977,16 +999,6 @@ def make_uv_chunk(uv_array):
return uv_chunk
'''
def make_matrix_4x3_chunk(matrix):
matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
for vec in matrix.col:
for f in vec[:3]:
matrix_chunk.add_variable("matrix_f", _3ds_float(f))
return matrix_chunk
'''
def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
"""Make a chunk out of a Blender mesh."""
@ -1017,8 +1029,6 @@ def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
if uv_array:
mesh_chunk.add_subchunk(make_uv_chunk(uv_array))
# mesh_chunk.add_subchunk(make_matrix_4x3_chunk(matrix))
# create transformation matrix chunk
matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
obj_matrix = matrix.transposed().to_3x3()
@ -1073,7 +1083,6 @@ def make_kfdata(start=0, stop=0, curtime=0):
def make_track_chunk(ID, obj):
"""Make a chunk for track data.
Depending on the ID, this will construct a position, rotation or scale track."""
track_chunk = _3ds_chunk(ID)
track_chunk.add_variable("track_flags", _3ds_ushort())
@ -1111,13 +1120,12 @@ def make_track_chunk(ID, obj):
def make_kf_obj_node(obj, name_to_id):
"""Make a node chunk for a Blender object.
Takes the Blender object as a parameter. Object id's are taken from the dictionary name_to_id.
Blender Empty objects are converted to dummy nodes."""
name = obj.name
# main object node chunk:
kf_obj_node = _3ds_chunk(KFDATA_OBJECT_NODE_TAG)
kf_obj_node = _3ds_chunk(OBJECT_NODE_TAG)
# chunk for the object id:
obj_id_chunk = _3ds_chunk(OBJECT_NODE_ID)
# object id is from the name_to_id dictionary:
@ -1184,7 +1192,6 @@ def save(operator,
# Time the export
duration = time.time()
# Blender.Window.WaitCursor(1)
if global_matrix is None:
global_matrix = mathutils.Matrix()
@ -1237,6 +1244,7 @@ def save(operator,
else:
objects = [ob for ob in scene.objects if not ob.hide_viewport]
empty_objects = [ob for ob in objects if ob.type == 'EMPTY']
light_objects = [ob for ob in objects if ob.type == 'LIGHT']
camera_objects = [ob for ob in objects if ob.type == 'CAMERA']
@ -1304,15 +1312,17 @@ def save(operator,
object_info.add_subchunk(make_material_chunk(ma_image[0], ma_image[1]))
# Give all objects a unique ID and build a dictionary from object name to object id:
translation = {} # collect translation for transformation matrix
# name_to_id = {}
translation = {} # collect translation for transformation matrix
for ob, data, matrix in mesh_objects:
translation[ob.name] = ob.location
# name_to_id[ob.name]= len(name_to_id)
"""
#for ob in empty_objects:
for ob in empty_objects:
translation[ob.name] = ob.location
# name_to_id[ob.name]= len(name_to_id)
"""
# Create object chunks for all meshes:
i = 0
@ -1339,10 +1349,6 @@ def save(operator,
kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
'''
# if not blender_mesh.users:
# bpy.data.meshes.remove(blender_mesh)
# blender_mesh.vertices = None
i += i
# Create chunks for all empties:
@ -1362,7 +1368,7 @@ def save(operator,
object_chunk.add_variable("light", _3ds_string(sane_name(ob.name)))
light_chunk.add_variable("location", _3ds_point_3d(ob.location))
color_float_chunk.add_variable("color", _3ds_float_color(ob.data.color))
energy_factor.add_variable("energy", _3ds_float(ob.data.energy * .001))
energy_factor.add_variable("energy", _3ds_float(ob.data.energy * 0.001))
light_chunk.add_subchunk(color_float_chunk)
light_chunk.add_subchunk(energy_factor)
@ -1374,12 +1380,18 @@ def save(operator,
pos_y = ob.location[1] + (ob.location[0] * math.tan(math.radians(90) - ob.rotation_euler[2]))
pos_z = hypo * math.tan(math.radians(90) - ob.rotation_euler[0])
spotlight_chunk = _3ds_chunk(LIGHT_SPOTLIGHT)
spot_roll_chunk = _3ds_chunk(LIGHT_SPOTROLL)
spot_roll_chunk = _3ds_chunk(LIGHT_SPOT_ROLL)
spotlight_chunk.add_variable("target", _3ds_point_3d((pos_x, pos_y, pos_z)))
spotlight_chunk.add_variable("hotspot", _3ds_float(round(hotspot, 4)))
spotlight_chunk.add_variable("angle", _3ds_float(round(cone_angle, 4)))
spot_roll_chunk.add_variable("roll", _3ds_float(round(ob.rotation_euler[1], 6)))
spotlight_chunk.add_subchunk(spot_roll_chunk)
if ob.data.show_cone:
spot_cone_chunk = _3ds_chunk(LIGHT_SPOT_SEE_CONE)
spotlight_chunk.add_subchunk(spot_cone_chunk)
if ob.data.use_square:
spot_square_chunk = _3ds_chunk(LIGHT_SPOT_RECTANGLE)
spotlight_chunk.add_subchunk(spot_square_chunk)
light_chunk.add_subchunk(spotlight_chunk)
# Add light to object info
@ -1411,9 +1423,9 @@ def save(operator,
'''
# At this point, the chunk hierarchy is completely built.
# Check the size:
primary.get_size()
# Open the file for writing:
file = open(filepath, 'wb')
@ -1428,7 +1440,6 @@ def save(operator,
name_mapping.clear()
# Debugging only: report the exporting time:
# Blender.Window.WaitCursor(0)
print("3ds export time: %.2f" % (time.time() - duration))
# Debugging only: dump the chunk hierarchy:

File diff suppressed because it is too large Load Diff

View File

@ -931,26 +931,6 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
me.edges.foreach_get("vertices", t_ev)
me.loops.foreach_get("edge_index", t_lei)
# Polygons might not be in the same order as loops. To export per-loop and per-polygon data in a matching order,
# one must be set into the order of the other. Since there are fewer polygons than loops and there are usually
# more geometry layers exported that are per-loop than per-polygon, it's more efficient to re-order polygons and
# per-polygon data.
perm_polygons_to_loop_order = None
# t_ls indicates the ordering of polygons compared to loops. When t_ls is sorted, polygons and loops are in the same
# order. Since each loop must be assigned to exactly one polygon for the mesh to be valid, every value in t_ls must
# be unique, so t_ls will be monotonically increasing when sorted.
# t_ls is expected to be in the same order as loops in most cases since exiting Edit mode will sort t_ls, so do an
# initial check for any element being smaller than the previous element to determine if sorting is required.
sort_polygon_data = np.any(t_ls[1:] < t_ls[:-1])
if sort_polygon_data:
# t_ls is not sorted, so get the indices that would sort t_ls using argsort, these will be re-used to sort
# per-polygon data.
# Using 'stable' for radix sort, which performs much better with partially ordered data and slightly worse with
# completely random data, compared to the default of 'quicksort' for introsort.
perm_polygons_to_loop_order = np.argsort(t_ls, kind='stable')
# Sort t_ls into the same order as loops.
t_ls = t_ls[perm_polygons_to_loop_order]
# Add "fake" faces for loose edges. Each "fake" face consists of two loops creating a new 2-sided polygon.
if scene_data.settings.use_mesh_edges:
bl_edge_is_loose_dtype = bool
@ -1051,8 +1031,6 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
if smooth_type == 'FACE':
t_ps = np.empty(len(me.polygons), dtype=poly_use_smooth_dtype)
me.polygons.foreach_get("use_smooth", t_ps)
if sort_polygon_data:
t_ps = t_ps[perm_polygons_to_loop_order]
_map = b"ByPolygon"
else: # EDGE
_map = b"ByEdge"
@ -1071,17 +1049,14 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
# Get the 'use_smooth' attribute of all polygons.
p_use_smooth_mask = np.empty(mesh_poly_nbr, dtype=poly_use_smooth_dtype)
me.polygons.foreach_get('use_smooth', p_use_smooth_mask)
if sort_polygon_data:
p_use_smooth_mask = p_use_smooth_mask[perm_polygons_to_loop_order]
# Invert to get all flat shaded polygons.
p_flat_mask = np.invert(p_use_smooth_mask, out=p_use_smooth_mask)
# Convert flat shaded polygons to flat shaded loops by repeating each element by the number of sides of
# that polygon.
# Polygon sides can be calculated from the element-wise difference of sorted loop starts appended by the
# number of loops. Alternatively, polygon sides can be retrieved directly from the 'loop_total'
# attribute of polygons, but that might need to be sorted, and we already have t_ls which is sorted loop
# starts. It tends to be quicker to calculate from t_ls when above around 10_000 polygons even when the
# 'loop_total' array wouldn't need sorting.
# Polygon sides can be calculated from the element-wise difference of loop starts appended by the number
# of loops. Alternatively, polygon sides can be retrieved directly from the 'loop_total' attribute of
# polygons, but since we already have t_ls, it tends to be quicker to calculate from t_ls when above
# around 10_000 polygons.
polygon_sides = np.diff(mesh_t_ls_view, append=mesh_loop_nbr)
p_flat_loop_mask = np.repeat(p_flat_mask, polygon_sides)
# Convert flat shaded loops to flat shaded (sharp) edge indices.
@ -1442,8 +1417,6 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
fbx_pm_dtype = np.int32
t_pm = np.empty(len(me.polygons), dtype=bl_pm_dtype)
me.polygons.foreach_get("material_index", t_pm)
if sort_polygon_data:
t_pm = t_pm[perm_polygons_to_loop_order]
# We have to validate mat indices, and map them to FBX indices.
# Note a mat might not be in me_fbxmaterials_idx (e.g. node mats are ignored).
@ -1474,7 +1447,6 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
elem_data_single_string(lay_ma, b"MappingInformationType", b"AllSame")
elem_data_single_string(lay_ma, b"ReferenceInformationType", b"IndexToDirect")
elem_data_single_int32_array(lay_ma, b"Materials", [0])
del perm_polygons_to_loop_order
# And the "layer TOC"...

View File

@ -372,7 +372,7 @@ def blen_read_custom_properties(fbx_obj, blen_obj, settings):
def blen_read_object_transform_do(transform_data):
# This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
#
# WorldTransform = ParentWorldTransform @ T @ Roff @ Rp @ Rpre @ R @ Rpost @ Rp-1 @ Soff @ Sp @ S @ Sp-1
# WorldTransform = ParentWorldTransform @ T @ Roff @ Rp @ Rpre @ R @ Rpost-1 @ Rp-1 @ Soff @ Sp @ S @ Sp-1
#
# Where all those terms are 4 x 4 matrices that contain:
# WorldTransform: Transformation matrix of the node in global space.
@ -382,7 +382,7 @@ def blen_read_object_transform_do(transform_data):
# Rp: Rotation pivot
# Rpre: Pre-rotation
# R: Rotation
# Rpost: Post-rotation
# Rpost-1: Inverse of the post-rotation (FBX 2011 documentation incorrectly specifies this without inversion)
# Rp-1: Inverse of the rotation pivot
# Soff: Scaling offset
# Sp: Scaling pivot
@ -402,14 +402,15 @@ def blen_read_object_transform_do(transform_data):
# S: Scaling
# OT: Geometric transform translation
# OR: Geometric transform rotation
# OS: Geometric transform translation
# OS: Geometric transform scale
#
# Notes:
# Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
# of WorldTransform's parent node.
# The R matrix takes into account the rotation order. Other rotation matrices are always 'XYZ' order.
#
# Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/
# index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429
# Taken from https://help.autodesk.com/view/FBX/2020/ENU/
# ?guid=FBX_Developer_Help_nodes_and_scene_graph_fbx_nodes_computing_transformation_matrix_html
# translation
lcl_translation = Matrix.Translation(transform_data.loc)
@ -418,9 +419,9 @@ def blen_read_object_transform_do(transform_data):
# rotation
to_rot = lambda rot, rot_ord: Euler(convert_deg_to_rad_iter(rot), rot_ord).to_matrix().to_4x4()
lcl_rot = to_rot(transform_data.rot, transform_data.rot_ord) @ transform_data.rot_alt_mat
pre_rot = to_rot(transform_data.pre_rot, transform_data.rot_ord)
pst_rot = to_rot(transform_data.pst_rot, transform_data.rot_ord)
geom_rot = to_rot(transform_data.geom_rot, transform_data.rot_ord)
pre_rot = to_rot(transform_data.pre_rot, 'XYZ')
pst_rot = to_rot(transform_data.pst_rot, 'XYZ')
geom_rot = to_rot(transform_data.geom_rot, 'XYZ')
rot_ofs = Matrix.Translation(transform_data.rot_ofs)
rot_piv = Matrix.Translation(transform_data.rot_piv)
@ -439,7 +440,7 @@ def blen_read_object_transform_do(transform_data):
rot_piv @
pre_rot @
lcl_rot @
pst_rot @
pst_rot.inverted_safe() @
rot_piv.inverted_safe() @
sca_ofs @
sca_piv @

View File

@ -4,7 +4,7 @@
bl_info = {
'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (3, 6, 14),
"version": (3, 6, 15),
'blender': (3, 5, 0),
'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0',
@ -450,7 +450,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_hierarchy_flatten_bones: BoolProperty(
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
)
@ -458,16 +458,18 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
name='Optimize Animation Size',
description=(
"Reduce exported file size by removing duplicate keyframes"
"(can cause problems with stepped animation)"
),
default=True
)
export_optimize_animation_keep_anim_armature: BoolProperty(
name='Force keeping channel for armature / bones',
name='Force keeping channels for bones',
description=(
"if all keyframes are identical in a rig "
"force keeping the minimal animation"
"if all keyframes are identical in a rig, "
"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
)
@ -475,7 +477,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_optimize_animation_keep_anim_object: BoolProperty(
name='Force keeping channel for objects',
description=(
"if all keyframes are identical for object transformations "
"If all keyframes are identical for object transformations, "
"force keeping the minimal animation"
),
default=False
@ -488,7 +490,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
('CROP', 'Crop',
'Keep only frames above frame 0'),
),
description='Negative Frames are slided or cropped',
description='Negative Frames are slid or cropped',
default='SLIDE'
)
@ -496,7 +498,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
name='Set all glTF Animation starting at 0',
description=(
"Set all glTF animation starting at 0.0s. "
"Can be usefull for looping animations"
"Can be useful for looping animations"
),
default=False
)
@ -505,7 +507,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
name='Bake All Objects Animations',
description=(
"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"
),
default=False
@ -1225,11 +1227,38 @@ class GLTF_PT_export_animation(bpy.types.Panel):
row.active = operator.export_morph is True
row.prop(operator, 'export_morph_animation')
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')
if operator.export_animation_mode == "SCENE":
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):
bl_space_type = 'FILE_BROWSER'
@ -1256,8 +1285,12 @@ class GLTF_PT_export_animation_ranges(bpy.types.Panel):
layout.active = operator.export_animations
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')
row = layout.row()
row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'NLA_TRACKS']
layout.prop(operator, 'export_negative_frame')
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):
sfile = context.space_data
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="")
def draw(self, context):
@ -1347,11 +1380,9 @@ class GLTF_PT_export_animation_optimize(bpy.types.Panel):
layout.prop(operator, 'export_optimize_animation_size')
row = layout.row()
row.active = operator.export_optimize_animation_size
row.prop(operator, 'export_optimize_animation_keep_anim_armature')
row = layout.row()
row.active = operator.export_optimize_animation_size
row.prop(operator, 'export_optimize_animation_keep_anim_object')
@ -1641,6 +1672,7 @@ classes = (
GLTF_PT_export_data_lighting,
GLTF_PT_export_data_compression,
GLTF_PT_export_animation,
GLTF_PT_export_animation_notes,
GLTF_PT_export_animation_ranges,
GLTF_PT_export_animation_armature,
GLTF_PT_export_animation_sampling,

View File

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

View File

@ -21,10 +21,13 @@ class NODE_OT_GLTF_SETTINGS(bpy.types.Operator):
@classmethod
def poll(cls, context):
space = context.space_data
return space.type == "NODE_EDITOR" \
and context.object and context.object.active_material \
and context.object.active_material.use_nodes is True \
return (
space is not None
and space.type == "NODE_EDITOR"
and context.object and context.object.active_material
and context.object.active_material.use_nodes is True
and bpy.context.preferences.addons['io_scene_gltf2'].preferences.settings_node_ui is True
)
def execute(self, context):
gltf_settings_node_name = get_gltf_node_name()

View File

@ -143,7 +143,7 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
# Check if the property can be exported without sampling
new_properties = {}
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 :)
else:
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)
def __needs_baking(obj_uuid: str,
def needs_baking(obj_uuid: str,
channels: typing.Tuple[bpy.types.FCurve],
export_settings
) -> bool:

View File

@ -6,6 +6,7 @@ import typing
import mathutils
from .....io.com import gltf2_io
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.gltf2_io_user_extensions import export_user_extensions
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]
# Select the interpolation method.
return {
"BEZIER": "CUBICSPLINE",
"LINEAR": "LINEAR",
"CONSTANT": "STEP"
}[blender_keyframe.interpolation]
return get_gltf_interpolation(blender_keyframe.interpolation)

View File

@ -6,6 +6,7 @@ import typing
from ....io.com import gltf2_io
from ....io.com.gltf2_io_debug import print_console
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_extras import generate_extras
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)
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])
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] = {}
# 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)
for (obj_uuid, type_, prop, bone) in to_be_sampled:
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":
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":
channel = gather_sampled_sk_channel(obj_uuid, blender_action.name, export_settings)
else:

View File

@ -5,8 +5,10 @@ import bpy
import typing
from ......io.com import gltf2_io
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 ...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 ..object.gltf2_blender_gather_object_channels import gather_sampled_object_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]
# 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:
# Not bake situation
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 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"]:
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 p in ["location", "rotation_quaternion", "scale"]:
@ -40,7 +53,8 @@ def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_
bone,
p,
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)
if channel is not None:
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
# 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"]:
armature_channels = ["location", "rotation_quaternion", "scale"]
armature_channels = []
else:
armature_channels = __gather_armature_object_channel(bpy.data.actions[blender_action_name], export_settings)
for channel in armature_channels:
armature_channels = __gather_armature_object_channel(armature_uuid, bpy.data.actions[blender_action_name], export_settings)
for p in ["location", "rotation_quaternion", "scale"]:
armature_channel = gather_sampled_object_channel(
armature_uuid,
channel,
p,
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
)
@ -79,12 +95,13 @@ def gather_sampled_bone_channel(
channel: str,
action_name: str,
node_channel_is_animated: bool,
node_channel_interpolation: str,
export_settings
):
__target= __gather_target(armature_uuid, bone, channel, export_settings)
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:
# 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(
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(
armature_uuid,
bone,
channel,
action_name,
node_channel_is_animated,
node_channel_interpolation,
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 = []
for p in ["location", "rotation_quaternion", "scale", "delta_location", "delta_scale", "delta_rotation_euler", "delta_rotation_quaternion"]:
if p in [f.data_path for f in blender_action.fcurves]:
channels_animated, to_be_sampled = get_channel_groups(obj_uuid, blender_action, export_settings)
# Remove all channel linked to bones, keep only directly object channels
channels_animated = [c for c in channels_animated.values() if c['type'] == "OBJECT"]
to_be_sampled = [c for c in to_be_sampled if c[1] == "OBJECT"]
original_channels = []
for c in channels_animated:
original_channels.extend([(prop, c['properties'][prop][0].keyframe_points[0].interpolation) for prop in c['properties'].keys()])
for c, inter in original_channels:
channels.append(
(
{
"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(p)
}.get(c),
get_gltf_interpolation(inter)
)
)
return list(set(channels)) #remove doubles
for c in to_be_sampled:
channels.append(
(
{
"location":"location",
"rotation_quaternion": "rotation_quaternion",
"rotation_euler": "rotation_quaternion",
"scale": "scale",
"delta_location": "location",
"delta_scale": "scale",
"delta_rotation_euler": "rotation_quaternion",
"delta_rotation_quaternion": "rotation_quaternion"
}.get(c[2]),
get_gltf_interpolation("LINEAR") # Forced to be sampled, so use LINEAR
)
)
return channels

View File

@ -53,8 +53,21 @@ def gather_bone_sampled_keyframes(
return None
if not export_settings['gltf_optimize_animation']:
# For bones, if all values are the same, keeping only if changing values, or if user want to keep data
if node_channel_is_animated is True:
return keyframes # Always keeping
else:
# baked bones
if export_settings['gltf_optimize_animation_keep_armature'] is False:
# Not keeping if not changing property
cst = fcurve_is_constant(keyframes)
return None if cst is True else keyframes
else:
# Keep data, as requested by user. We keep all samples, as user don't want to optimize
return keyframes
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,

View File

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

View File

@ -5,6 +5,7 @@ import bpy
import typing
from ......io.com import gltf2_io
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 ....gltf2_blender_gather_cache import cached
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]:
channels = []
list_of_animated_channels = []
list_of_animated_channels = {}
if object_uuid != blender_action_name and blender_action_name in bpy.data.actions:
# Not bake situation
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 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"]:
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"]:
channel = gather_sampled_object_channel(
object_uuid,
p,
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
)
if channel is not None:
@ -48,12 +52,13 @@ def gather_sampled_object_channel(
channel: str,
action_name: str,
node_channel_is_animated: bool,
node_channel_interpolation: str,
export_settings
):
__target= __gather_target(obj_uuid, channel, export_settings)
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:
# After check, no need to animate this node for this channel
@ -92,6 +97,7 @@ def __gather_sampler(
channel: str,
action_name: str,
node_channel_is_animated: bool,
node_channel_interpolation: str,
export_settings):
@ -100,5 +106,6 @@ def __gather_sampler(
channel,
action_name,
node_channel_is_animated,
node_channel_interpolation,
export_settings
)

View File

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

View File

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

View File

@ -3,7 +3,7 @@
bl_info = {
"name": "Node Wrangler",
"author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
"version": (3, 45),
"version": (3, 46),
"blender": (3, 6, 0),
"location": "Node Editor Toolbar or Shift-W",
"description": "Various tools to enhance and speed up node-based workflow",

View File

@ -13,6 +13,7 @@ from bpy.props import (
CollectionProperty,
)
from bpy_extras.io_utils import ImportHelper, ExportHelper
from bpy_extras.node_utils import connect_sockets
from mathutils import Vector
from os import path
from glob import glob
@ -368,13 +369,13 @@ class NWSwapLinks(Operator, NWBase):
for connection in n1_outputs:
try:
links.new(n2.outputs[connection[0]], connection[1])
connect_sockets(n2.outputs[connection[0]], connection[1])
except:
self.report({'WARNING'},
"Some connections have been lost due to differing numbers of output sockets")
for connection in n2_outputs:
try:
links.new(n1.outputs[connection[0]], connection[1])
connect_sockets(n1.outputs[connection[0]], connection[1])
except:
self.report({'WARNING'},
"Some connections have been lost due to differing numbers of output sockets")
@ -412,8 +413,8 @@ class NWSwapLinks(Operator, NWBase):
i1t = pair[0].links[0].to_socket
i2f = pair[1].links[0].from_socket
i2t = pair[1].links[0].to_socket
links.new(i1f, i2t)
links.new(i2f, i1t)
connect_sockets(i1f, i2t)
connect_sockets(i2f, i1t)
if t[1] == 1:
if len(types) == 1:
fs = t[0].links[0].from_socket
@ -424,14 +425,14 @@ class NWSwapLinks(Operator, NWBase):
i += 1
while n1.inputs[i].is_linked:
i += 1
links.new(fs, n1.inputs[i])
connect_sockets(fs, n1.inputs[i])
elif len(types) == 2:
i1f = types[0][0].links[0].from_socket
i1t = types[0][0].links[0].to_socket
i2f = types[1][0].links[0].from_socket
i2t = types[1][0].links[0].to_socket
links.new(i1f, i2t)
links.new(i2f, i1t)
connect_sockets(i1f, i2t)
connect_sockets(i2f, i1t)
else:
self.report({'WARNING'}, "This node has no input connections to swap!")
@ -702,7 +703,7 @@ class NWPreviewNode(Operator, NWBase):
make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex]))
output_socket = geometryoutput.inputs[geometryoutindex]
for li_from, li_to in make_links:
base_node_tree.links.new(li_from, li_to)
connect_sockets(li_from, li_to)
tree = base_node_tree
link_end = output_socket
while tree.nodes.active != active:
@ -713,11 +714,11 @@ class NWPreviewNode(Operator, NWBase):
node_socket = node.node_tree.outputs[index]
if node_socket in delete_sockets:
delete_sockets.remove(node_socket)
tree.links.new(link_start, link_end)
connect_sockets(link_start, link_end)
# Iterate
link_end = self.ensure_group_output(node.node_tree).inputs[index]
tree = tree.nodes.active.node_tree
tree.links.new(active.outputs[out_i], link_end)
connect_sockets(active.outputs[out_i], link_end)
# Delete sockets
for socket in delete_sockets:
@ -776,7 +777,7 @@ class NWPreviewNode(Operator, NWBase):
make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
output_socket = materialout.inputs[materialout_index]
for li_from, li_to in make_links:
base_node_tree.links.new(li_from, li_to)
connect_sockets(li_from, li_to)
# Create links through node groups until we reach the active node
tree = base_node_tree
@ -789,11 +790,11 @@ class NWPreviewNode(Operator, NWBase):
node_socket = node.node_tree.outputs[index]
if node_socket in delete_sockets:
delete_sockets.remove(node_socket)
tree.links.new(link_start, link_end)
connect_sockets(link_start, link_end)
# Iterate
link_end = self.ensure_group_output(node.node_tree).inputs[index]
tree = tree.nodes.active.node_tree
tree.links.new(active.outputs[out_i], link_end)
connect_sockets(active.outputs[out_i], link_end)
# Delete sockets
for socket in delete_sockets:
@ -1064,31 +1065,31 @@ class NWSwitchNodeType(Operator, NWBase):
if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
in_src_link = node.inputs[src_i].links[0]
in_dst_socket = new_node.inputs[dst_i]
links.new(in_src_link.from_socket, in_dst_socket)
connect_sockets(in_src_link.from_socket, in_dst_socket)
links.remove(in_src_link)
# OUTPUTS: Base on matches in proper order.
for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
for out_src_link in node.outputs[src_i].links:
out_dst_socket = new_node.outputs[dst_i]
links.new(out_dst_socket, out_src_link.to_socket)
connect_sockets(out_dst_socket, out_src_link.to_socket)
# relink rest inputs if possible, no criteria
for src_inp in node.inputs:
for dst_inp in new_node.inputs:
if src_inp.links and not dst_inp.links:
src_link = src_inp.links[0]
links.new(src_link.from_socket, dst_inp)
connect_sockets(src_link.from_socket, dst_inp)
links.remove(src_link)
# relink rest outputs if possible, base on node kind if any left.
for src_o in node.outputs:
for out_src_link in src_o.links:
for dst_o in new_node.outputs:
if src_o.type == dst_o.type:
links.new(dst_o, out_src_link.to_socket)
connect_sockets(dst_o, out_src_link.to_socket)
# relink rest outputs no criteria if any left. Link all from first output.
for src_o in node.outputs:
for out_src_link in src_o.links:
if new_node.outputs:
links.new(new_node.outputs[0], out_src_link.to_socket)
connect_sockets(new_node.outputs[0], out_src_link.to_socket)
nodes.remove(node)
force_update(context)
return {'FINISHED'}
@ -1177,16 +1178,16 @@ class NWMergeNodes(Operator, NWBase):
# outputs to the multi input socket.
if i < len(socket_indices) - 1:
ind = socket_indices[i]
links.new(node.outputs[0], new_node.inputs[ind])
connect_sockets(node.outputs[0], new_node.inputs[ind])
else:
outputs_for_multi_input.insert(0, node.outputs[0])
if outputs_for_multi_input != []:
ind = socket_indices[-1]
for output in outputs_for_multi_input:
links.new(output, new_node.inputs[ind])
connect_sockets(output, new_node.inputs[ind])
if prev_links != []:
for link in prev_links:
links.new(new_node.outputs[0], link.to_node.inputs[0])
connect_sockets(new_node.outputs[0], link.to_node.inputs[0])
return new_node
def execute(self, context):
@ -1447,19 +1448,19 @@ class NWMergeNodes(Operator, NWBase):
# Prevent cyclic dependencies when nodes to be merged are linked to one another.
# Link only if "to_node" index not in invalid indexes list.
if not self.link_creates_cycle(ss_link, invalid_nodes):
links.new(get_first_enabled_output(last_add), ss_link.to_socket)
connect_sockets(get_first_enabled_output(last_add), ss_link.to_socket)
# add links from last_add to all links 'to_socket' of out links of first selected.
for fs_link in first_selected_output.links:
# Link only if "to_node" index not in invalid indexes list.
if not self.link_creates_cycle(fs_link, invalid_nodes):
links.new(get_first_enabled_output(last_add), fs_link.to_socket)
connect_sockets(get_first_enabled_output(last_add), fs_link.to_socket)
# add link from "first" selected and "first" add node
node_to = nodes[count_after - 1]
links.new(first_selected_output, node_to.inputs[first])
connect_sockets(first_selected_output, node_to.inputs[first])
if node_to.type == 'ZCOMBINE':
for fs_out in first_selected.outputs:
if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
links.new(fs_out, node_to.inputs[1])
connect_sockets(fs_out, node_to.inputs[1])
break
# add links between added ADD nodes and between selected and ADD nodes
for i in range(count_adds):
@ -1468,21 +1469,21 @@ class NWMergeNodes(Operator, NWBase):
node_to = nodes[index - 1]
node_to_input_i = first
node_to_z_i = 1 # if z combine - link z to first z input
links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
if node_to.type == 'ZCOMBINE':
for from_out in node_from.outputs:
if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
links.new(from_out, node_to.inputs[node_to_z_i])
connect_sockets(from_out, node_to.inputs[node_to_z_i])
if len(nodes_list) > 1:
node_from = nodes[nodes_list[i + 1][0]]
node_to = nodes[index]
node_to_input_i = second
node_to_z_i = 3 # if z combine - link z to second z input
links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
connect_sockets(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
if node_to.type == 'ZCOMBINE':
for from_out in node_from.outputs:
if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
links.new(from_out, node_to.inputs[node_to_z_i])
connect_sockets(from_out, node_to.inputs[node_to_z_i])
index -= 1
# set "last" of added nodes as active
nodes.active = last_add
@ -1690,7 +1691,7 @@ class NWCopySettings(Operator, NWBase):
new_node.location = node_loc
for str_from, str_to in reconnections:
node_tree.links.new(eval(str_from), eval(str_to))
node_tree.connect_sockets(eval(str_from), eval(str_to))
success_names.append(new_node.name)
@ -1859,7 +1860,7 @@ class NWAddTextureSetup(Operator, NWBase):
x_offset = x_offset + image_texture_node.width + padding
image_texture_node.location = [locx - x_offset, locy]
nodes.active = image_texture_node
links.new(image_texture_node.outputs[0], target_input)
connect_sockets(image_texture_node.outputs[0], target_input)
# The mapping setup following this will connect to the first input of this image texture.
target_input = image_texture_node.inputs[0]
@ -1871,7 +1872,7 @@ class NWAddTextureSetup(Operator, NWBase):
mapping_node = nodes.new('ShaderNodeMapping')
x_offset = x_offset + mapping_node.width + padding
mapping_node.location = [locx - x_offset, locy]
links.new(mapping_node.outputs[0], target_input)
connect_sockets(mapping_node.outputs[0], target_input)
# Add Texture Coordinates node.
tex_coord_node = nodes.new('ShaderNodeTexCoord')
@ -1881,7 +1882,7 @@ class NWAddTextureSetup(Operator, NWBase):
is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
use_generated_coordinates = is_procedural_texture or use_environment_texture
tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
links.new(tex_coord_output, mapping_node.inputs[0])
connect_sockets(tex_coord_output, mapping_node.inputs[0])
return {'FINISHED'}
@ -2006,7 +2007,7 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
disp_node = nodes.new(type='ShaderNodeDisplacement')
# Align the Displacement node under the active Principled BSDF node
disp_node.location = active_node.location + Vector((100, -700))
link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
link = connect_sockets(disp_node.inputs[0], disp_texture.outputs[0])
# TODO Turn on true displacement in the material
# Too complicated for now
@ -2015,7 +2016,7 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
if output_node:
if not output_node[0].inputs[2].is_linked:
link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
link = connect_sockets(output_node[0].inputs[2], disp_node.outputs[0])
continue
@ -2045,13 +2046,13 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
if match_normal:
# If Normal add normal node in between
normal_node = nodes.new(type='ShaderNodeNormalMap')
link = links.new(normal_node.inputs[1], texture_node.outputs[0])
link = connect_sockets(normal_node.inputs[1], texture_node.outputs[0])
elif match_bump:
# If Bump add bump node in between
normal_node = nodes.new(type='ShaderNodeBump')
link = links.new(normal_node.inputs[2], texture_node.outputs[0])
link = connect_sockets(normal_node.inputs[2], texture_node.outputs[0])
link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
link = connect_sockets(active_node.inputs[sname[0]], normal_node.outputs[0])
normal_node_texture = texture_node
elif sname[0] == 'Roughness':
@ -2062,19 +2063,19 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
if match_rough:
# If Roughness nothing to to
link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
elif match_gloss:
# If Gloss Map add invert node
invert_node = nodes.new(type='ShaderNodeInvert')
link = links.new(invert_node.inputs[1], texture_node.outputs[0])
link = connect_sockets(invert_node.inputs[1], texture_node.outputs[0])
link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
link = connect_sockets(active_node.inputs[sname[0]], invert_node.outputs[0])
roughness_node = texture_node
else:
# This is a simple connection Texture --> Input slot
link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
# Use non-color for all but 'Base Color' Textures
if not sname[0] in ['Base Color', 'Emission'] and texture_node.image:
@ -2119,15 +2120,15 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
sum(n.location.y for n in texture_nodes) / len(texture_nodes)))
reroute.location = tex_coords + Vector((-50, -120))
for texture_node in texture_nodes:
link = links.new(texture_node.inputs[0], reroute.outputs[0])
link = links.new(reroute.inputs[0], mapping.outputs[0])
link = connect_sockets(texture_node.inputs[0], reroute.outputs[0])
link = connect_sockets(reroute.inputs[0], mapping.outputs[0])
else:
link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
link = connect_sockets(texture_nodes[0].inputs[0], mapping.outputs[0])
# Connect texture_coordiantes to mapping node
texture_input = nodes.new(type='ShaderNodeTexCoord')
texture_input.location = mapping.location + Vector((-200, 0))
link = links.new(mapping.inputs[0], texture_input.outputs[2])
link = connect_sockets(mapping.inputs[0], texture_input.outputs[2])
# Create frame around tex coords and mapping
frame = nodes.new(type='NodeFrame')
@ -2231,8 +2232,8 @@ class NWAddReroutes(Operator, NWBase):
n = nodes.new('NodeReroute')
nodes.active = n
for link in output.links:
links.new(n.outputs[0], link.to_socket)
links.new(output, n.inputs[0])
connect_sockets(n.outputs[0], link.to_socket)
connect_sockets(output, n.inputs[0])
n.location = loc
post_select.append(n)
reroutes_count += 1
@ -2324,7 +2325,7 @@ class NWLinkActiveToSelected(Operator, NWBase):
for input in node.inputs:
if input.type == out.type or node.type == 'REROUTE':
if replace or not input.is_linked:
links.new(out, input)
connect_sockets(out, input)
if not use_node_name and not use_outputs_names:
doit = False
break
@ -2521,7 +2522,7 @@ class NWLinkToOutputNode(Operator):
elif tree_type == 'GeometryNodeTree':
if active.outputs[output_index].type != 'GEOMETRY':
return {'CANCELLED'}
links.new(active.outputs[output_index], output_node.inputs[out_input_index])
connect_sockets(active.outputs[output_index], output_node.inputs[out_input_index])
force_update(context) # viewport render does not update
@ -2542,7 +2543,7 @@ class NWMakeLink(Operator, NWBase):
n1 = nodes[context.scene.NWLazySource]
n2 = nodes[context.scene.NWLazyTarget]
links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
connect_sockets(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
force_update(context)
@ -2566,7 +2567,7 @@ class NWCallInputsMenu(Operator, NWBase):
if len(n2.inputs) > 1:
bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
elif len(n2.inputs) == 1:
links.new(n1.outputs[self.from_socket], n2.inputs[0])
connect_sockets(n1.outputs[self.from_socket], n2.inputs[0])
return {'FINISHED'}
@ -2950,7 +2951,7 @@ class NWResetNodes(bpy.types.Operator):
new_node.location = node_loc
for str_from, str_to in reconnections:
node_tree.links.new(eval(str_from), eval(str_to))
connect_sockets(eval(str_from), eval(str_to))
new_node.select = False
success_names.append(new_node.name)

View File

@ -8,7 +8,8 @@ bl_info = {
"location": "3D View",
"description": "Distribute object instances on another object.",
"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',
"category": "Object",
}

View File

@ -16,8 +16,8 @@
bl_info = {
"name": "Sun Position",
"author": "Michael Martin, Damien Picard",
"version": (3, 3, 3),
"blender": (3, 0, 0),
"version": (3, 5, 0),
"blender": (3, 2, 0),
"location": "World > Sun Position",
"description": "Show sun position with objects and/or sky texture",
"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(
(properties.SunPosProperties,
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))
@persistent
def sun_scene_handler(scene):
sun_props = bpy.context.scene.sun_pos_properties
# Force drawing update
sun_props.show_surface = sun_props.show_surface
sun_props.show_analemmas = sun_props.show_analemmas
sun_props.show_north = sun_props.show_north
# Force coordinates update
sun_props.latitude = sun_props.latitude
def register():
register_classes()

View File

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

View File

@ -95,9 +95,9 @@ def draw_callback_px(self, context):
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_label = "Sync Sun to Texture"
bl_label = "Pick Sun in Viewport"
exposure: FloatProperty(name="Exposure", 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
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')
return {'CANCELLED'}

View File

@ -4,9 +4,12 @@ import bpy
from bpy.types import AddonPreferences, PropertyGroup
from bpy.props import (StringProperty, EnumProperty, IntProperty,
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 datetime import datetime
@ -16,6 +19,47 @@ TODAY = datetime.today()
# 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):
usage_mode: EnumProperty(
@ -36,42 +80,49 @@ class SunPosProperties(PropertyGroup):
use_refraction: BoolProperty(
name="Use Refraction",
description="Show apparent Sun position due to refraction",
description="Show the apparent Sun position due to atmospheric refraction",
default=True,
update=sun_update)
show_north: BoolProperty(
name="Show North",
description="Draw line pointing north",
description="Draw a line pointing to the north",
default=False,
update=north_update)
north_offset: FloatProperty(
name="North Offset",
description="Rotate the scene to choose North direction",
description="Rotate the scene to choose the North direction",
unit="ROTATION",
soft_min=-pi, soft_max=pi, step=10.0, default=0.0,
update=sun_update)
show_surface: BoolProperty(
name="Show Surface",
description="Draw sun surface",
description="Draw the surface that the Sun occupies in the sky",
default=False,
update=surface_update)
show_analemmas: BoolProperty(
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,
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(
name="Latitude",
description="Latitude: (+) Northern (-) Southern",
soft_min=-90.0, soft_max=90.0,
step=5, precision=3,
default=0.0,
update=sun_update)
update=lat_long_update)
longitude: FloatProperty(
name="Longitude",
@ -79,7 +130,7 @@ class SunPosProperties(PropertyGroup):
soft_min=-180.0, soft_max=180.0,
step=5, precision=3,
default=0.0,
update=sun_update)
update=lat_long_update)
sunrise_time: FloatProperty(
name="Sunrise Time",
@ -95,24 +146,23 @@ class SunPosProperties(PropertyGroup):
default=0.0,
get=lambda _: sun.sunset)
sun_azimuth: FloatProperty(
name="Sun Azimuth",
description="Rotation angle of the Sun from the north direction",
soft_min=-pi, soft_max=pi,
default=0.0,
get=lambda _: sun.azimuth)
sun_elevation: FloatProperty(
name="Sunset Time",
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)
co_parser: StringProperty(
name="Enter coordinates",
description="Enter coordinates from an online map",
update=parse_coordinates)
sun_azimuth: FloatProperty(
name="Sun Azimuth",
description="Rotation angle of the Sun from the direction of the north",
soft_min=-pi, soft_max=pi,
precision=3,
default=0.0,
unit="ROTATION",
get=lambda _: sun.azimuth - bpy.context.scene.sun_pos_properties.north_offset)
month: IntProperty(
name="Month",
@ -130,19 +180,19 @@ class SunPosProperties(PropertyGroup):
update=sun_update)
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",
default=False,
update=sun_update)
day_of_year: IntProperty(
name="Day of year",
name="Day of Year",
min=1, max=366, default=1,
update=sun_update)
UTC_zone: FloatProperty(
name="UTC zone",
description="Time zone: Difference from Greenwich, England in hours",
name="UTC Zone",
description="Difference from Greenwich, England, in hours",
precision=1,
min=-14.0, max=13, step=50, default=0.0,
update=sun_update)
@ -156,7 +206,7 @@ class SunPosProperties(PropertyGroup):
sun_distance: FloatProperty(
name="Distance",
description="Distance to sun from origin",
description="Distance to the Sun from the origin",
unit="LENGTH",
min=0.0, soft_max=3000.0, step=10.0, default=50.0,
update=sun_update)
@ -164,22 +214,22 @@ class SunPosProperties(PropertyGroup):
sun_object: PointerProperty(
name="Sun 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',
update=sun_update)
object_collection: PointerProperty(
name="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)
object_collection_type: EnumProperty(
name="Display type",
description="Show object collection as sun motion",
description="Type of Sun motion to visualize.",
items=(
('ANALEMMA', "Analemma", ""),
('DIURNAL', "Diurnal", ""),
('ANALEMMA', "Analemma", "Trajectory of the Sun in the sky during the year, for a given time of the day"),
('DIURNAL', "Diurnal", "Trajectory of the Sun in the sky during a single day"),
),
default='ANALEMMA',
update=sun_update)
@ -187,19 +237,19 @@ class SunPosProperties(PropertyGroup):
sky_texture: StringProperty(
name="Sky Texture",
default="",
description="Name of sky texture to be used",
description="Name of the sky texture to use",
update=sun_update)
hdr_texture: StringProperty(
default="Environment Texture",
name="Environment Texture",
description="Name of texture to use. World nodes must be enabled "
"and color set to Environment Texture",
description="Name of the environment texture to use. World nodes must be enabled "
"and the color set to an environment Texture",
update=sun_update)
hdr_azimuth: FloatProperty(
name="Rotation",
description="Rotation angle of sun and environment texture",
description="Rotation angle of the Sun and environment texture",
unit="ROTATION",
step=10.0,
default=0.0, precision=3,
@ -207,7 +257,7 @@ class SunPosProperties(PropertyGroup):
hdr_elevation: FloatProperty(
name="Elevation",
description="Elevation angle of sun",
description="Elevation angle of the Sun",
unit="ROTATION",
step=10.0,
default=0.0, precision=3,
@ -215,13 +265,13 @@ class SunPosProperties(PropertyGroup):
bind_to_sun: BoolProperty(
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,
update=sun_update)
time_spread: FloatProperty(
name="Time Spread",
description="Time period in which to spread object collection",
description="Time period around which to spread object collection",
precision=4,
soft_min=1.0, soft_max=24.0, step=1.0, default=23.0,
update=sun_update)
@ -234,53 +284,24 @@ class SunPosProperties(PropertyGroup):
class SunPosAddonPreferences(AddonPreferences):
bl_idname = __package__
show_time_place: BoolProperty(
name="Time and place presets",
description="Show time/place presets",
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",
show_overlays: BoolProperty(
name="Show Overlays",
description="Display overlays in the viewport: the direction of the north, analemmas and the Sun surface",
default=True,
update=sun_update)
show_refraction: BoolProperty(
name="Refraction",
description="Show sun refraction choice",
default=True,
update=sun_update)
description="Show Sun Refraction choice",
default=True)
show_az_el: BoolProperty(
name="Azimuth and elevation info",
name="Azimuth and Elevation Info",
description="Show azimuth and solar elevation info",
default=True)
show_daylight_savings: BoolProperty(
name="Daylight savings",
description="Show daylight savings time choice",
default=True,
update=sun_update)
show_rise_set: BoolProperty(
name="Sunrise and sunset info",
name="Sunrise and Sunset Info",
description="Show sunrise and sunset labels",
default=True)
@ -292,12 +313,7 @@ class SunPosAddonPreferences(AddonPreferences):
col.label(text="Show options or labels:")
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_overlays")
flow.prop(self, "show_az_el")
flow.prop(self, "show_daylight_savings")
flow.prop(self, "show_rise_set")

View File

@ -2,6 +2,7 @@
import bpy
from bpy.app.handlers import persistent
import gpu
from gpu_extras.batch import batch_for_shader
@ -9,7 +10,6 @@ from mathutils import Euler, Vector
from math import degrees, radians, pi, sin, cos, asin, acos, tan, floor
import datetime
from .geo import parse_position
class SunInfo:
@ -48,32 +48,6 @@ class 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):
"""
Cycle through all the selected objects and set their position and rotation
@ -120,8 +94,7 @@ def move_sun(context):
azimuth, elevation = get_sun_coordinates(
local_time, sun_props.latitude, sun_props.longitude,
zone, sun_props.month, sun_props.day, sun_props.year,
sun_props.sun_distance)
zone, sun_props.month, sun_props.day, sun_props.year)
sun.azimuth = azimuth
sun.elevation = elevation
@ -159,8 +132,7 @@ def move_sun(context):
azimuth, elevation = get_sun_coordinates(
local_time, sun_props.latitude,
sun_props.longitude, zone,
sun_props.month, sun_props.day,
sun_props.year, sun_props.sun_distance)
sun_props.month, sun_props.day)
obj.location = get_sun_vector(azimuth, elevation) * sun_props.sun_distance
local_time -= time_increment
obj.rotation_euler = ((elevation - pi/2, 0, -azimuth))
@ -174,8 +146,7 @@ def move_sun(context):
azimuth, elevation = get_sun_coordinates(
local_time, sun_props.latitude,
sun_props.longitude, zone,
dt.month, dt.day, sun_props.year,
sun_props.sun_distance)
dt.month, dt.day, sun_props.year)
obj.location = get_sun_vector(azimuth, elevation) * sun_props.sun_distance
day -= day_increment
obj.rotation_euler = (elevation - pi/2, 0, -azimuth)
@ -225,50 +196,46 @@ def sun_handler(scene):
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 daylight_savings:
UTC_zone += 1
the_time -= UTC_zone
time -= UTC_zone
the_time %= 24
time %= 24
hh = int(the_time)
mm = (the_time - int(the_time)) * 60
ss = int((mm - int(mm)) * 60)
return ("%02i:%02i:%02i" % (hh, mm, ss))
return format_hms(time)
def format_hms(the_time):
hh = str(int(the_time))
min = (the_time - int(the_time)) * 60
sec = int((min - int(min)) * 60)
mm = "0" + str(int(min)) if min < 10 else str(int(min))
ss = "0" + str(sec) if sec < 10 else str(sec)
def format_hms(time):
hh = int(time)
mm = (time % 1.0) * 60
ss = (mm % 1.0) * 60
return (hh + ":" + mm + ":" + ss)
return f"{hh:02d}:{int(mm):02d}:{int(ss):02d}"
def format_lat_long(lat_long, is_latitude):
hh = str(abs(int(lat_long)))
min = abs((lat_long - int(lat_long)) * 60)
sec = abs(int((min - int(min)) * 60))
mm = "0" + str(int(min)) if min < 10 else str(int(min))
ss = "0" + str(sec) if sec < 10 else str(sec)
if lat_long == 0:
coord_tag = " "
def format_lat_long(latitude, longitude):
coordinates = ""
for i, co in enumerate((latitude, longitude)):
dd = abs(int(co))
mm = abs(co - int(co)) * 60.0
ss = abs(mm - int(mm)) * 60.0
if co == 0:
direction = ""
elif i == 0:
direction = "N" if co > 0 else "S"
else:
if is_latitude:
coord_tag = " N" if lat_long > 0 else " S"
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,
utc_zone, month, day, year, distance):
utc_zone, month, day, year):
"""
Calculate the actual position of the sun based on input parameters.
@ -284,7 +251,6 @@ def get_sun_coordinates(local_time, latitude, longitude,
NOAA's web site is:
http://www.esrl.noaa.gov/gmd/grad/solcalc
"""
addon_prefs = bpy.context.preferences.addons[__package__].preferences
sun_props = bpy.context.scene.sun_pos_properties
longitude *= -1 # for internal calculations
@ -446,10 +412,6 @@ def calc_sunrise_sunset(rise):
sun.latitude, sun.longitude)
time_local = new_time_UTC + (-zone * 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:
time_local += 60.0
tl = time_local / 60.0
@ -563,7 +525,7 @@ def calc_surface(context):
def get_surface_coordinates(time, month):
azimuth, elevation = get_sun_coordinates(
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) * sun_props.sun_distance
sun_vector.z = max(0, sun_vector.z)
return sun_vector
@ -588,71 +550,8 @@ def calc_analemma(context, h):
day, month = day_of_year_to_month_day(sun_props.year, day_of_year)
azimuth, elevation = get_sun_coordinates(
h, sun_props.latitude, sun_props.longitude,
zone, month, day, sun_props.year,
sun_props.sun_distance)
zone, month, day, sun_props.year)
sun_vector = get_sun_vector(azimuth, elevation) * sun_props.sun_distance
if sun_vector.z > 0:
vertices.append(sun_vector)
return vertices
def draw_surface(batch, shader):
blend = gpu.state.blend_get()
gpu.state.blend_set("ALPHA")
shader.uniform_float("color", (.8, .6, 0, 0.2))
batch.draw(shader)
gpu.state.blend_set(blend)
def draw_analemmas(batch, shader):
shader.uniform_float("color", (1, 0, 0, 1))
batch.draw(shader)
_handle_surface = None
def surface_update(self, context):
global _handle_surface
if self.show_surface:
coords = calc_surface(context)
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'TRIS', {"pos": coords})
if _handle_surface is not None:
bpy.types.SpaceView3D.draw_handler_remove(_handle_surface, 'WINDOW')
_handle_surface = bpy.types.SpaceView3D.draw_handler_add(
draw_surface, (batch, shader), 'WINDOW', 'POST_VIEW')
elif _handle_surface is not None:
bpy.types.SpaceView3D.draw_handler_remove(_handle_surface, 'WINDOW')
_handle_surface = None
_handle_analemmas = None
def analemmas_update(self, context):
global _handle_analemmas
if self.show_analemmas:
coords = []
indices = []
coord_offset = 0
for h in range(24):
analemma_verts = calc_analemma(context, h)
coords.extend(analemma_verts)
for i in range(len(analemma_verts) - 1):
indices.append((coord_offset + i,
coord_offset + i+1))
coord_offset += len(analemma_verts)
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'LINES',
{"pos": coords}, indices=indices)
if _handle_analemmas is not None:
bpy.types.SpaceView3D.draw_handler_remove(_handle_analemmas, 'WINDOW')
_handle_analemmas = bpy.types.SpaceView3D.draw_handler_add(
draw_analemmas, (batch, shader), 'WINDOW', 'POST_VIEW')
elif _handle_analemmas is not None:
bpy.types.SpaceView3D.draw_handler_remove(_handle_analemmas, 'WINDOW')
_handle_analemmas = None

View File

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

View File

@ -3,10 +3,11 @@
import bpy
from bpy.types import Operator, Menu
from bl_operators.presets import AddPresetBase
from bl_ui.utils import PresetPanel
import os
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"
preset_subdir = "operator/sun_position"
preset_operator = "script.execute_preset"
draw = Menu.draw_preset
preset_add_operator = "world.sunpos_add_preset"
class SUNPOS_OT_AddPreset(AddPresetBase, Operator):
'''Add Sun Position preset'''
bl_idname = "world.sunpos_add_preset"
bl_label = "Add Sun Position preset"
preset_menu = "SUNPOS_MT_Presets"
preset_menu = "SUNPOS_PT_Presets"
# variable used for all preset values
preset_defines = [
@ -61,91 +62,33 @@ class SUNPOS_PT_Panel(bpy.types.Panel):
bl_label = "Sun Position"
bl_options = {'DEFAULT_CLOSED'}
def draw_header_preset(self, _context):
SUNPOS_PT_Presets.draw_panel_header(self.layout)
def draw(self, context):
sp = context.scene.sun_pos_properties
p = context.preferences.addons[__package__].preferences
sun_props = context.scene.sun_pos_properties
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):
col = self.layout.column(align=True)
col.label(text="Usage Mode")
row = col.row()
row.prop(sp, "usage_mode", expand=True)
col.separator()
if sp.usage_mode == "HDR":
self.draw_environ_mode_panel(context, sp, p, layout)
layout.prop(sun_props, "usage_mode", expand=True)
layout.separator()
if sun_props.usage_mode == "HDR":
self.draw_environment_mode_panel(context)
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):
flow = layout.grid_flow(row_major=True, columns=0, even_columns=True,
even_rows=False, align=False)
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
def draw_environment_mode_panel(self, context):
sun_props = context.scene.sun_pos_properties
layout = self.layout
col = layout.column(align=True)
col.use_property_split = True
col.use_property_decorate = False
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()
col.prop_search(sun_props, "sun_object",
context.view_layer, "objects")
if context.scene.world 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")
else:
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.",
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):
bl_space_type = "PROPERTIES"
@ -164,68 +160,34 @@ class SUNPOS_PT_Location(bpy.types.Panel):
@classmethod
def poll(self, context):
sp = context.scene.sun_pos_properties
return sp.usage_mode != "HDR"
sun_props = context.scene.sun_pos_properties
return sun_props.usage_mode != "HDR"
def draw(self, context):
layout = self.layout
sp = context.scene.sun_pos_properties
p = context.preferences.addons[__package__].preferences
layout.use_property_split = True
sun_props = context.scene.sun_pos_properties
addon_prefs = context.preferences.addons[__package__].preferences
col = layout.column(align=True)
col.label(text="Enter Coordinates")
col.prop(sp, "co_parser", text='', icon='URL')
col.prop(sun_props, "coordinates", 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()
if p.show_north:
col = flow.column(align=True)
col.prop(sp, "show_north", toggle=True)
col.prop(sp, "north_offset")
col = layout.column(align=True)
col.prop(sun_props, "north_offset", text="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()
if p.show_surface or p.show_analemmas:
col = flow.column(align=True)
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 = layout.column()
col.prop(sun_props, "sun_distance")
col.separator()
@ -238,63 +200,67 @@ class SUNPOS_PT_Time(bpy.types.Panel):
@classmethod
def poll(self, context):
sp = context.scene.sun_pos_properties
return sp.usage_mode != "HDR"
sun_props = context.scene.sun_pos_properties
return sun_props.usage_mode != "HDR"
def draw(self, context):
layout = self.layout
sp = context.scene.sun_pos_properties
p = context.preferences.addons[__package__].preferences
layout.use_property_split = True
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.prop(sp, "use_day_of_year",
icon='SORTTIME')
if sp.use_day_of_year:
col.prop(sp, "day_of_year")
col = layout.column(align=True)
col.prop(sun_props, "use_day_of_year")
if sun_props.use_day_of_year:
col.prop(sun_props, "day_of_year")
else:
col.prop(sp, "day")
col.prop(sp, "month")
col.prop(sp, "year")
col.prop(sun_props, "day")
col.prop(sun_props, "month")
col.prop(sun_props, "year")
col.separator()
col = flow.column(align=True)
col.prop(sp, "time")
col.prop(sp, "UTC_zone")
if p.show_daylight_savings:
col.prop(sp, "use_daylight_savings")
col = layout.column(align=True)
col.prop(sun_props, "time", text="Time", text_ctxt="Hour")
col.prop(sun_props, "UTC_zone")
col.prop(sun_props, "use_daylight_savings")
col.separator()
col = flow.column(align=True)
lt = format_time(sp.time,
p.show_daylight_savings and sp.use_daylight_savings,
sp.longitude)
ut = format_time(sp.time,
p.show_daylight_savings and sp.use_daylight_savings,
sp.longitude,
sp.UTC_zone)
local_time = format_time(sun_props.time,
sun_props.use_daylight_savings)
utc_time = format_time(sun_props.time,
sun_props.use_daylight_savings,
sun_props.UTC_zone)
col = layout.column(align=True)
col.alignment = 'CENTER'
split = col.split(factor=0.5, align=True)
split.label(text="Local:", icon='TIME')
split.label(text=lt)
split = col.split(factor=0.5, align=True)
split.label(text="UTC:", icon='PREVIEW_RANGE')
split.label(text=ut)
sub = split.column(align=True)
sub.alignment = 'RIGHT'
sub.label(text="Time Local:")
sub.label(text="UTC:")
sub = split.column(align=True)
sub.label(text=local_time)
sub.label(text=utc_time)
col.separator()
col = flow.column(align=True)
if addon_prefs.show_rise_set:
sunrise = format_hms(sun.sunrise)
sunset = format_hms(sun.sunset)
col = layout.column(align=True)
col.alignment = 'CENTER'
if p.show_rise_set:
sr = format_hms(sun.sunrise)
ss = format_hms(sun.sunset)
split = col.split(factor=0.5, align=True)
split.label(text="Sunrise:", icon='LIGHT_SUN')
split.label(text=sr)
split = col.split(factor=0.5, align=True)
split.label(text="Sunset:", icon='SOLO_ON')
split.label(text=ss)
sub = split.column(align=True)
sub.alignment = 'RIGHT'
sub.label(text="Sunrise:")
sub.label(text="Sunset:")
sub = split.column(align=True)
sub.label(text=sunrise)
sub.label(text=sunset)
col.separator()

View File

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

View File

@ -141,25 +141,34 @@ class UI_OT_i18n_cleanuptranslation_svn_branches(Operator):
def i18n_updatetranslation_svn_trunk_callback(lng, settings):
reports = []
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']))
return lng['uid'], 0.0
reports.append("Skipping {} language ({}), edit settings if you want to enable it.".format(lng['name'], lng['uid']))
return lng['uid'], 0.0, reports
if not lng['use']:
print("Skipping {} language ({}).\n".format(lng['name'], lng['uid']))
return lng['uid'], 0.0
reports.append("Skipping {} language ({}).".format(lng['name'], lng['uid']))
return lng['uid'], 0.0, reports
po = utils_i18n.I18nMessages(uid=lng['uid'], kind='PO', src=lng['po_path'], settings=settings)
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()) +
("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:
po.write(kind="PO", dest=lng['po_path_trunk'][:-3] + "_raw.po")
po.rtl_process()
po.write(kind="PO", dest=lng['po_path_trunk'])
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()
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):
@ -178,12 +187,13 @@ class UI_OT_i18n_updatetranslation_svn_trunk(Operator):
context.window_manager.progress_update(0)
with concurrent.futures.ProcessPoolExecutor() as exctr:
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],
(self.settings,) * num_langs,
chunksize=4)):
context.window_manager.progress_update(progress + 1)
stats[lng_uid] = stats_val
print("".join(reports) + "\n")
# Copy pot file from branches to trunk.
shutil.copy2(self.settings.FILE_NAME_POT, self.settings.TRUNK_PO_DIR)