Fixed branch for Blender 3.6 release #104626

Closed
Sebastian Sille wants to merge 22 commits from (deleted):blender-v3.6-release into blender-v3.6-release

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

View File

@ -16,7 +16,7 @@ import bpy
bl_info = {
"name": "Autodesk 3DS format",
"author": "Bob Holcomb, Campbell Barton, Andreas Atteneder, Sebastian Schrand",
"version": (2, 3, 4),
"version": (2, 4, 1),
"blender": (3, 6, 0),
"location": "File > Import-Export",
"description": "3DS Import/Export meshes, UVs, materials, textures, "
@ -65,12 +65,12 @@ class Import3DS(bpy.types.Operator, ImportHelper):
"importing incorrectly",
default=True,
)
read_keyframe: bpy.props.BoolProperty(
read_keyframe: BoolProperty(
name="Read Keyframe",
description="Read the keyframe data",
default=True,
)
use_world_matrix: bpy.props.BoolProperty(
use_world_matrix: BoolProperty(
name="World Space",
description="Transform to matrix world",
default=False,
@ -109,6 +109,11 @@ class Export3DS(bpy.types.Operator, ExportHelper):
description="Export selected objects only",
default=False,
)
write_keyframe: BoolProperty(
name="Write Keyframe",
description="Write the keyframe data",
default=False,
)
def execute(self, context):
from . import export_3ds

View File

@ -121,6 +121,7 @@ KFDATA_KFCURTIME = 0xB009 # Frame current
KFDATA_KFHDR = 0xB00A # Keyframe header
# >------ sub defines of OBJECT_NODE_TAG
PARENT_NAME = 0x80F0 # Object parent name tree
OBJECT_NODE_ID = 0xB030 # Object hierachy ID
OBJECT_NODE_HDR = 0xB010 # Hierachy tree header
OBJECT_INSTANCE_NAME = 0xB011 # Object instance name
@ -149,7 +150,7 @@ def sane_name(name):
if name_fixed is not None:
return name_fixed
# strip non ascii chars
# Strip non ascii chars
new_name_clean = new_name = name.encode("ASCII", "replace").decode("ASCII")[:12]
i = 0
@ -157,7 +158,7 @@ def sane_name(name):
new_name = new_name_clean + '.%.3d' % i
i += 1
# note, appending the 'str' version
# Note, appending the 'str' version
name_unique.append(new_name)
name_mapping[name] = new_name = new_name.encode("ASCII", "replace")
return new_name
@ -166,7 +167,7 @@ def sane_name(name):
def uv_key(uv):
return round(uv[0], 6), round(uv[1], 6)
# size defines
# Size defines
SZ_SHORT = 2
SZ_INT = 4
SZ_FLOAT = 4
@ -238,7 +239,7 @@ class _3ds_string(object):
file.write(struct.pack(binary_format, self.value))
def __str__(self):
return str(self.value)
return str((self.value).decode("ASCII"))
class _3ds_point_3d(object):
@ -259,11 +260,11 @@ class _3ds_point_3d(object):
# Used for writing a track
'''
class _3ds_point_4d(object):
"""Class representing a four-dimensional point for a 3ds file, for instance a quaternion."""
__slots__ = "w", "x", "y", "z"
def __init__(self, point=(0.0,0.0,0.0,0.0)):
def __init__(self, point):
self.w, self.x, self.y, self.z = point
def get_size(self):
@ -275,7 +276,6 @@ class _3ds_point_4d(object):
def __str__(self):
return '(%f, %f, %f, %f)' % (self.w, self.x, self.y, self.z)
'''
class _3ds_point_uv(object):
@ -341,7 +341,7 @@ class _3ds_face(object):
def get_size(self):
return 4 * SZ_SHORT
# no need to validate every face vert, the oversized array will catch this problem
# No need to validate every face vert, the oversized array will catch this problem
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))
@ -353,13 +353,14 @@ class _3ds_face(object):
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"
def __init__(self):
self.values = []
self.size = SZ_SHORT
# add an item
# Add an item
def add(self, item):
self.values.append(item)
self.size += item.get_size()
@ -383,7 +384,6 @@ class _3ds_array(object):
class _3ds_named_variable(object):
"""Convenience class for named variables."""
__slots__ = "value", "name"
def __init__(self, name, val=None):
@ -408,11 +408,11 @@ class _3ds_named_variable(object):
self.value)
# the chunk class
# 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.
"""
Chunks contain zero or more variables, followed by zero or more subchunks."""
__slots__ = "ID", "size", "variables", "subchunks"
def __init__(self, chunk_id=0):
@ -460,7 +460,7 @@ class _3ds_chunk(object):
"""Write the chunk to a file.
Uses the write function of the variables and the subchunks to do the actual work."""
# write header
# Write header
self.ID.write(file)
self.size.write(file)
for variable in self.variables:
@ -481,9 +481,9 @@ class _3ds_chunk(object):
subchunk.dump(indent + 1)
##########
# EXPORT #
##########
#############
# MATERIALS #
#############
def get_material_image(material):
""" Get images from paint slots."""
@ -514,7 +514,7 @@ def make_material_subchunk(chunk_id, color):
col1 = _3ds_chunk(RGB1)
col1.add_variable("color1", _3ds_rgb_color(color))
mat_sub.add_subchunk(col1)
# optional:
# Optional
# col2 = _3ds_chunk(RGBI)
# col2.add_variable("color2", _3ds_rgb_color(color))
# mat_sub.add_subchunk(col2)
@ -527,7 +527,7 @@ 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:
# Optional
# pctf = _3ds_chunk(PCTF)
# pctf.add_variable("pctfloat", _3ds_float(round(percent, 6)))
# pct_sub.add_subchunk(pctf)
@ -579,7 +579,7 @@ def make_material_texture_chunk(chunk_id, texslots, pct):
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 tintflags are present"""
either 0x100 or 0x200, tintcolor will be processed if a tintflag is present"""
mapflags = 0
if texslot.extension == 'EXTEND':
@ -628,7 +628,7 @@ def make_material_texture_chunk(chunk_id, texslots, pct):
rgb.add_variable("mapcolor", _3ds_rgb_color(spec if texslot.socket_dst.identifier == 'Specular' else base))
mat_sub.add_subchunk(rgb)
# store all textures for this mapto in order. This at least is what the
# Store all textures for this mapto in order. This at least is what the
# 3DS exporter did so far, afaik most readers will just skip over 2nd textures
for slot in texslots:
if slot.image is not None:
@ -730,7 +730,7 @@ def make_material_chunk(material, image):
if matmap:
material_chunk.add_subchunk(matmap)
# make sure no textures are lost. Everything that doesn't fit
# Make sure no textures are lost. Everything that doesn't fit
# into a channel is exported as secondary texture
diffuse = []
@ -757,7 +757,7 @@ def make_material_chunk(material, image):
material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - material.diffuse_color[3]))
material_chunk.add_subchunk(shading)
slots = [get_material_image(material)] # can be None
slots = [get_material_image(material)] # Can be None
if image:
material_chunk.add_subchunk(make_texture_chunk(MAT_DIFFUSEMAP, slots))
@ -765,6 +765,10 @@ def make_material_chunk(material, image):
return material_chunk
#############
# MESH DATA #
#############
class tri_wrapper(object):
"""Class representing a triangle.
Used when converting faces to triangles"""
@ -776,7 +780,7 @@ class tri_wrapper(object):
self.ma = ma
self.image = image
self.faceuvs = faceuvs
self.offset = [0, 0, 0] # offset indices
self.offset = [0, 0, 0] # Offset indices
self.flag = flag
self.group = group
@ -844,13 +848,13 @@ def remove_face_uv(verts, tri_list):
need to be converted to vertex uv coordinates. That means that vertices need to be duplicated when
there are multiple uv coordinates per vertex."""
# initialize a list of UniqueLists, one per vertex
# Initialize a list of UniqueLists, one per vertex
unique_uvs = [{} for i in range(len(verts))]
# for each face uv coordinate, add it to the UniqueList of the vertex
# For each face uv coordinate, add it to the UniqueList of the vertex
for tri in tri_list:
for i in range(3):
# store the index into the UniqueList for future reference:
# Store the index into the UniqueList for future reference
# offset.append(uv_list[tri.vertex_index[i]].add(_3ds_point_uv(tri.faceuvs[i])))
context_uv_vert = unique_uvs[tri.vertex_index[i]]
@ -862,7 +866,7 @@ def remove_face_uv(verts, tri_list):
tri.offset[i] = offset_index__uv_3ds[0]
# At this point, each vertex has a UniqueList containing every uv coordinate associated with it only once
# At this point each vertex has a UniqueList containing every uv coord associated with it only once
# Now we need to duplicate every vertex as many times as it has uv coordinates and make sure the
# faces refer to the new face indices
vert_index = 0
@ -875,16 +879,15 @@ def remove_face_uv(verts, tri_list):
pt = _3ds_point_3d(vert.co) # reuse, should be ok
uvmap = [None] * len(unique_uvs[i])
for ii, uv_3ds in unique_uvs[i].values():
# add a vertex duplicate to the vertex_array for every uv associated with this vertex
# Add a vertex duplicate to the vertex_array for every uv associated with this vertex
vert_array.add(pt)
# add the uv coordinate to the uv array, this for loop does not give
# Add the uv coordinate to the uv array, this for loop does not give
# uv's ordered by ii, so we create a new map and add the uv's later
# uv_array.add(uv_3ds)
uvmap[ii] = uv_3ds
# Add the uv's in the correct order
# Add uv's in the correct order and add coordinates to the uv array
for uv_3ds in uvmap:
# add the uv coordinate to the uv array
uv_array.add(uv_3ds)
vert_index += len(unique_uvs[i])
@ -1004,23 +1007,21 @@ def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
vert_array = _3ds_array()
for vert in mesh.vertices:
vert_array.add(_3ds_point_3d(vert.co))
# no UV at all
# No UV at all
uv_array = None
# create the chunk
# Create the chunk
mesh_chunk = _3ds_chunk(OBJECT_MESH)
# add vertex chunk
# Add vertex and faces chunk
mesh_chunk.add_subchunk(make_vert_chunk(vert_array))
# add faces chunk
mesh_chunk.add_subchunk(make_faces_chunk(tri_list, mesh, materialDict))
# if available, add uv chunk
# If available, add uv chunk
if uv_array:
mesh_chunk.add_subchunk(make_uv_chunk(uv_array))
# create transformation matrix chunk
# Create transformation matrix chunk
matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
obj_matrix = matrix.transposed().to_3x3()
@ -1048,16 +1049,17 @@ def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
return mesh_chunk
''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
def make_kfdata(start=0, stop=0, curtime=0):
#################
# KEYFRAME DATA #
#################
def make_kfdata(revision, start=0, stop=100, curtime=0):
"""Make the basic keyframe data chunk"""
kfdata = _3ds_chunk(KFDATA)
kfhdr = _3ds_chunk(KFDATA_KFHDR)
kfhdr.add_variable("revision", _3ds_ushort(0))
# Not really sure what filename is used for, but it seems it is usually used
# to identify the program that generated the .3ds:
kfhdr.add_variable("filename", _3ds_string("Blender"))
kfhdr.add_variable("revision", _3ds_ushort(revision))
kfhdr.add_variable("filename", _3ds_string(b'Blender'))
kfhdr.add_variable("animlen", _3ds_uint(stop - start))
kfseg = _3ds_chunk(KFDATA_KFSEG)
@ -1072,107 +1074,397 @@ def make_kfdata(start=0, stop=0, curtime=0):
kfdata.add_subchunk(kfcurtime)
return kfdata
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."""
def make_track_chunk(ID, ob, ob_pos, ob_rot, ob_size):
"""Make a chunk for track data. Depending on the ID, this will
construct a position, rotation, scale, roll, color or fov track."""
track_chunk = _3ds_chunk(ID)
track_chunk.add_variable("track_flags", _3ds_ushort())
track_chunk.add_variable("unknown", _3ds_uint())
track_chunk.add_variable("unknown", _3ds_uint())
if ID in {POS_TRACK_TAG, ROT_TRACK_TAG, SCL_TRACK_TAG, ROLL_TRACK_TAG} and ob.animation_data and ob.animation_data.action:
action = ob.animation_data.action
if action.fcurves:
fcurves = action.fcurves
kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
nkeys = len(kframes)
if not 0 in kframes:
kframes.append(0)
nkeys = nkeys + 1
kframes = sorted(set(kframes))
track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
if ID == POS_TRACK_TAG: # Position
for i, frame in enumerate(kframes):
position = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'location']
if not position:
position.append(ob_pos)
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("position", _3ds_point_3d(position))
elif ID == ROT_TRACK_TAG: # Rotation
for i, frame in enumerate(kframes):
rotation = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'rotation_euler']
if not rotation:
rotation.append(ob_rot)
quat = mathutils.Euler(rotation).to_quaternion()
axis_angle = quat.angle, quat.axis[0], quat.axis[1], quat.axis[2]
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("rotation", _3ds_point_4d(axis_angle))
elif ID == SCL_TRACK_TAG: # Scale
for i, frame in enumerate(kframes):
size = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'scale']
if not size:
size.append(ob_size)
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("scale", _3ds_point_3d(size))
elif ID == ROLL_TRACK_TAG: # Roll
for i, frame in enumerate(kframes):
roll = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'rotation_euler']
if not roll:
roll.append(ob_rot)
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("roll", _3ds_float(round(math.degrees(roll[1]), 4)))
elif ID in {COL_TRACK_TAG, FOV_TRACK_TAG, HOTSPOT_TRACK_TAG, FALLOFF_TRACK_TAG} and ob.data.animation_data and ob.data.animation_data.action:
action = ob.data.animation_data.action
if action.fcurves:
fcurves = action.fcurves
kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
nkeys = len(kframes)
if not 0 in kframes:
kframes.append(0)
nkeys = nkeys + 1
kframes = sorted(set(kframes))
track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
if ID == COL_TRACK_TAG: # Color
for i, frame in enumerate(kframes):
color = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'color']
if not color:
color.append(ob.data.color[:3])
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("color", _3ds_float_color(color))
elif ID == FOV_TRACK_TAG: # Field of view
for i, frame in enumerate(kframes):
lens = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'lens']
if not lens:
lens.append(ob.data.lens)
fov = 2 * math.atan(ob.data.sensor_width/(2*lens[0]))
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("fov", _3ds_float(round(math.degrees(fov), 4)))
elif ID == HOTSPOT_TRACK_TAG: # Hotspot
beam_angle = math.degrees(ob.data.spot_size)
for i, frame in enumerate(kframes):
blend = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'spot_blend']
if not blend:
blend.append(ob.data.spot_blend)
hot_spot = beam_angle-(blend[0]*math.floor(beam_angle))
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("hotspot", _3ds_float(round(hot_spot, 4)))
elif ID == FALLOFF_TRACK_TAG: # Falloff
for i, frame in enumerate(kframes):
fall_off = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'spot_size']
if not fall_off:
fall_off.append(ob.data.spot_size)
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("falloff", _3ds_float(round(math.degrees(fall_off[0]), 4)))
else:
track_chunk.add_variable("track_flags", _3ds_ushort(0x40)) # Based on observation default flag is 0x40
track_chunk.add_variable("frame_start", _3ds_uint(0))
track_chunk.add_variable("frame_total", _3ds_uint(0))
track_chunk.add_variable("nkeys", _3ds_uint(1))
# Next section should be repeated for every keyframe, but for now, animation is not actually supported.
# Next section should be repeated for every keyframe, with no animation only one tag is needed
track_chunk.add_variable("tcb_frame", _3ds_uint(0))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
if obj.type=='Empty':
if ID==POS_TRACK_TAG:
# position vector:
track_chunk.add_variable("position", _3ds_point_3d(obj.getLocation()))
elif ID==ROT_TRACK_TAG:
# rotation (quaternion, angle first, followed by axis):
q = obj.getEuler().to_quaternion() # XXX, todo!
track_chunk.add_variable("rotation", _3ds_point_4d((q.angle, q.axis[0], q.axis[1], q.axis[2])))
elif ID==SCL_TRACK_TAG:
# scale vector:
track_chunk.add_variable("scale", _3ds_point_3d(obj.getSize()))
else:
# meshes have their transformations applied before
# exporting, so write identity transforms here:
if ID==POS_TRACK_TAG:
# position vector:
track_chunk.add_variable("position", _3ds_point_3d((0.0,0.0,0.0)))
elif ID==ROT_TRACK_TAG:
# rotation (quaternion, angle first, followed by axis):
track_chunk.add_variable("rotation", _3ds_point_4d((0.0, 1.0, 0.0, 0.0)))
elif ID==SCL_TRACK_TAG:
# scale vector:
track_chunk.add_variable("scale", _3ds_point_3d((1.0, 1.0, 1.0)))
# New method simply inserts the parameters
if ID == POS_TRACK_TAG: # Position vector
track_chunk.add_variable("position", _3ds_point_3d(ob_pos))
elif ID == ROT_TRACK_TAG: # Rotation (angle first [radians], followed by axis)
track_chunk.add_variable("rotation", _3ds_point_4d((ob_rot.angle, ob_rot.axis[0], ob_rot.axis[1], ob_rot.axis[2])))
elif ID == SCL_TRACK_TAG: # Scale vector
track_chunk.add_variable("scale", _3ds_point_3d(ob_size))
elif ID == ROLL_TRACK_TAG: # Roll angle
track_chunk.add_variable("roll", _3ds_float(round(math.degrees(ob.rotation_euler[1]), 4)))
elif ID == COL_TRACK_TAG: # Color values
track_chunk.add_variable("color", _3ds_float_color(ob.data.color))
elif ID == FOV_TRACK_TAG: # Field of view
track_chunk.add_variable("fov", _3ds_float(round(math.degrees(ob.data.angle), 4)))
elif ID == HOTSPOT_TRACK_TAG: # Hotspot
beam_angle = math.degrees(ob.data.spot_size)
track_chunk.add_variable("hotspot", _3ds_float(round(beam_angle-(ob.data.spot_blend*math.floor(beam_angle)), 4)))
elif ID == FALLOFF_TRACK_TAG: # Falloff
track_chunk.add_variable("falloff", _3ds_float(round(math.degrees(ob.data.spot_size), 4)))
return track_chunk
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.
def make_object_node(ob, translation, rotation, scale):
"""Make a node chunk for a Blender object. Takes Blender object as parameter.
Blender Empty objects are converted to dummy nodes."""
name = obj.name
# main object node chunk:
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:
obj_id_chunk.add_variable("node_id", _3ds_ushort(name_to_id[name]))
name = ob.name
if ob.type == 'CAMERA':
obj_node = _3ds_chunk(CAMERA_NODE_TAG)
elif ob.type == 'LIGHT':
obj_node = _3ds_chunk(LIGHT_NODE_TAG)
if ob.data.type == 'SPOT':
obj_node = _3ds_chunk(SPOT_NODE_TAG)
else: # Main object node chunk
obj_node = _3ds_chunk(OBJECT_NODE_TAG)
# object node header:
# Object node header with object name
obj_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
# object name:
if obj.type == 'Empty':
# Empties are called "$$$DUMMY" and use the OBJECT_INSTANCE_NAME chunk
# for their name (see below):
obj_node_header_chunk.add_variable("name", _3ds_string("$$$DUMMY"))
else:
# Add the name:
obj_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
# Add Flag variables (not sure what they do):
obj_node_header_chunk.add_variable("flags1", _3ds_ushort(0))
parent = ob.parent
if ob.type == 'EMPTY': # Forcing to use the real name for empties
# Empties called $$$DUMMY and use OBJECT_INSTANCE_NAME chunk as name
obj_node_header_chunk.add_variable("name", _3ds_string(b"$$$DUMMY"))
obj_node_header_chunk.add_variable("flags1", _3ds_ushort(0x4000))
obj_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
# Check parent-child relationships:
parent = obj.parent
if (parent is None) or (parent.name not in name_to_id):
# If no parent, or the parents name is not in the name_to_id dictionary,
# parent id becomes -1:
obj_node_header_chunk.add_variable("parent", _3ds_ushort(-1))
else: # Add flag variables - Based on observation flags1 is usually 0x0040 and 0x4000 for empty objects
obj_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
obj_node_header_chunk.add_variable("flags1", _3ds_ushort(0x0040))
# Flags2 defines bit 0x01 for display path, bit 0x02 use autosmooth, bit 0x04 object frozen,
# bit 0x10 for motion blur, bit 0x20 for material morph and bit 0x40 for mesh morph
if ob.type == 'MESH' and ob.data.use_auto_smooth:
obj_node_header_chunk.add_variable("flags2", _3ds_ushort(0x02))
else:
# Get the parent's id from the name_to_id dictionary:
obj_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
obj_node_header_chunk.add_variable("parent", _3ds_ushort(ROOT_OBJECT))
'''
# COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
# Check parent-child relationships:
if parent is None or parent.name not in name_to_id:
# If no parent, or parents name is not in dictionary, ID becomes -1:
obj_node_header_chunk.add_variable("parent", _3ds_ushort(-1))
else: # Get the parent's ID from the name_to_id dictionary:
obj_node_header_chunk.add_variable("parent", _3ds_ushort(name_to_id[parent.name]))
# Add pivot chunk:
obj_pivot_chunk = _3ds_chunk(OBJECT_PIVOT)
obj_pivot_chunk.add_variable("pivot", _3ds_point_3d(obj.getLocation()))
kf_obj_node.add_subchunk(obj_pivot_chunk)
# add subchunks for object id and node header:
kf_obj_node.add_subchunk(obj_id_chunk)
kf_obj_node.add_subchunk(obj_node_header_chunk)
# Empty objects need to have an extra chunk for the instance name:
if obj.type == 'Empty':
obj_instance_name_chunk = _3ds_chunk(OBJECT_INSTANCE_NAME)
obj_instance_name_chunk.add_variable("name", _3ds_string(sane_name(name)))
kf_obj_node.add_subchunk(obj_instance_name_chunk)
# Add track chunks for position, rotation and scale:
kf_obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, obj))
kf_obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, obj))
kf_obj_node.add_subchunk(make_track_chunk(SCL_TRACK_TAG, obj))
return kf_obj_node
'''
# Add subchunk for node header
obj_node.add_subchunk(obj_node_header_chunk)
# Empty objects need to have an extra chunk for the instance name
if ob.type == 'EMPTY': # Will use a real object name for empties for now
obj_instance_name_chunk = _3ds_chunk(OBJECT_INSTANCE_NAME)
obj_instance_name_chunk.add_variable("name", _3ds_string(sane_name(name)))
obj_node.add_subchunk(obj_instance_name_chunk)
if ob.type in {'MESH', 'EMPTY'}: # Add a pivot point at the object center
pivot_pos = (translation[name])
obj_pivot_chunk = _3ds_chunk(OBJECT_PIVOT)
obj_pivot_chunk.add_variable("pivot", _3ds_point_3d(pivot_pos))
obj_node.add_subchunk(obj_pivot_chunk)
# Create a bounding box from quadrant diagonal
obj_boundbox = _3ds_chunk(OBJECT_BOUNDBOX)
obj_boundbox.add_variable("min", _3ds_point_3d(ob.bound_box[0]))
obj_boundbox.add_variable("max", _3ds_point_3d(ob.bound_box[6]))
obj_node.add_subchunk(obj_boundbox)
# Add smooth angle if autosmooth is used
if ob.type == 'MESH' and ob.data.use_auto_smooth:
obj_morph_smooth = _3ds_chunk(OBJECT_MORPH_SMOOTH)
obj_morph_smooth.add_variable("angle", _3ds_float(round(ob.data.auto_smooth_angle, 6)))
obj_node.add_subchunk(obj_morph_smooth)
# Add track chunks for color, position, rotation and scale
if parent is None:
ob_pos = translation[name]
ob_rot = rotation[name]
ob_size = scale[name]
else: # Calculate child position and rotation of the object center, no scale applied
ob_pos = translation[name] - translation[parent.name]
ob_rot = rotation[name].cross(rotation[parent.name].copy().inverted())
ob_size = (1.0, 1.0, 1.0)
obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
if ob.type in {'MESH', 'EMPTY'}:
obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
obj_node.add_subchunk(make_track_chunk(SCL_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
if ob.type =='CAMERA':
obj_node.add_subchunk(make_track_chunk(FOV_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
obj_node.add_subchunk(make_track_chunk(ROLL_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
if ob.type =='LIGHT':
obj_node.add_subchunk(make_track_chunk(COL_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
if ob.type == 'LIGHT' and ob.data.type == 'SPOT':
obj_node.add_subchunk(make_track_chunk(HOTSPOT_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
obj_node.add_subchunk(make_track_chunk(FALLOFF_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
obj_node.add_subchunk(make_track_chunk(ROLL_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
return obj_node
def make_target_node(ob, translation, rotation, scale):
"""Make a target chunk for light and camera objects"""
name = ob.name
if ob.type == 'CAMERA': #Add camera target
tar_node = _3ds_chunk(TARGET_NODE_TAG)
elif ob.type == 'LIGHT': # Add spot target
tar_node = _3ds_chunk(LTARGET_NODE_TAG)
# Object node header with object name
tar_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
# Targets get the same name as the object, flags1 is usually 0x0010 and parent ROOT_OBJECT
tar_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
tar_node_header_chunk.add_variable("flags1", _3ds_ushort(0x0010))
tar_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
tar_node_header_chunk.add_variable("parent", _3ds_ushort(ROOT_OBJECT))
# Add subchunk for node header
tar_node.add_subchunk(tar_node_header_chunk)
# Calculate target position
ob_pos = translation[name]
ob_rot = rotation[name].to_euler()
ob_size = scale[name]
diagonal = math.copysign(math.sqrt(pow(ob_pos[0],2)+pow(ob_pos[1],2)), ob_pos[1])
target_x = ob_pos[0]+(ob_pos[1]*math.tan(ob_rot[2]))
target_y = ob_pos[1]+(ob_pos[0]*math.tan(math.radians(90)-ob_rot[2]))
target_z = -1*diagonal*math.tan(math.radians(90)-ob_rot[0])
# Add track chunks for target position
track_chunk = _3ds_chunk(POS_TRACK_TAG)
if ob.animation_data and ob.animation_data.action:
action = ob.animation_data.action
if action.fcurves:
fcurves = action.fcurves
kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
nkeys = len(kframes)
if not 0 in kframes:
kframes.append(0)
nkeys = nkeys + 1
kframes = sorted(set(kframes))
track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
for i, frame in enumerate(kframes):
target_pos = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'location']
target_rot = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'rotation_euler']
if not target_pos:
target_pos.append(ob_pos)
if not target_rot:
target_rot.insert(0, ob_rot.x)
target_rot.insert(1, ob_rot.y)
target_rot.insert(2, ob_rot.z)
diagonal = math.copysign(math.sqrt(pow(target_pos[0],2)+pow(target_pos[1],2)), target_pos[1])
target_x = target_pos[0]+(target_pos[1]*math.tan(target_rot[2]))
target_y = target_pos[1]+(target_pos[0]*math.tan(math.radians(90)-target_rot[2]))
target_z = -1*diagonal*math.tan(math.radians(90)-target_rot[0])
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("position", _3ds_point_3d((target_x, target_y, target_z)))
else: # Track header
track_chunk.add_variable("track_flags", _3ds_ushort(0x40)) # Based on observation default flag is 0x40
track_chunk.add_variable("frame_start", _3ds_uint(0))
track_chunk.add_variable("frame_total", _3ds_uint(0))
track_chunk.add_variable("nkeys", _3ds_uint(1))
# Keyframe header
track_chunk.add_variable("tcb_frame", _3ds_uint(0))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("position", _3ds_point_3d((target_x, target_y, target_z)))
tar_node.add_subchunk(track_chunk)
return tar_node
def make_ambient_node(world):
amb_color = world.color
amb_node = _3ds_chunk(AMBIENT_NODE_TAG)
track_chunk = _3ds_chunk(COL_TRACK_TAG)
# Object node header, name is "$AMBIENT$" for ambient nodes
amb_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
amb_node_header_chunk.add_variable("name", _3ds_string(b"$AMBIENT$"))
amb_node_header_chunk.add_variable("flags1", _3ds_ushort(0x4000)) # Flags1 0x4000 for empty objects
amb_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
amb_node_header_chunk.add_variable("parent", _3ds_ushort(ROOT_OBJECT))
amb_node.add_subchunk(amb_node_header_chunk)
if world.animation_data.action:
action = world.animation_data.action
if action.fcurves:
fcurves = action.fcurves
kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
nkeys = len(kframes)
if not 0 in kframes:
kframes.append(0)
nkeys = nkeys + 1
kframes = sorted(set(kframes))
track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
for i, frame in enumerate(kframes):
ambient = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'color']
if not ambient:
ambient.append(world.color)
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("color", _3ds_float_color(ambient))
else: # Track header
track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
track_chunk.add_variable("frame_start", _3ds_uint(0))
track_chunk.add_variable("frame_total", _3ds_uint(0))
track_chunk.add_variable("nkeys", _3ds_uint(1))
# Keyframe header
track_chunk.add_variable("tcb_frame", _3ds_uint(0))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("color", _3ds_float_color(amb_color))
amb_node.add_subchunk(track_chunk)
return amb_node
##########
# EXPORT #
##########
def save(operator,
context, filepath="",
use_selection=True,
use_selection=False,
write_keyframe=False,
global_matrix=None,
):
@ -1183,6 +1475,7 @@ def save(operator,
scene = context.scene
layer = context.view_layer
depsgraph = context.evaluated_depsgraph_get()
world = scene.world
if global_matrix is None:
global_matrix = mathutils.Matrix()
@ -1209,21 +1502,25 @@ def save(operator,
mscale.add_variable("scale", _3ds_float(1))
object_info.add_subchunk(mscale)
# Init main keyframe data chunk
if write_keyframe:
revision = 0x0005
stop = scene.frame_end
start = scene.frame_start
curtime = scene.frame_current
kfdata = make_kfdata(revision, start, stop, curtime)
# Add AMBIENT color
if scene.world is not None:
if world is not None:
ambient_chunk = _3ds_chunk(AMBIENTLIGHT)
ambient_light = _3ds_chunk(RGB)
ambient_light.add_variable("ambient", _3ds_float_color(scene.world.color))
ambient_chunk.add_subchunk(ambient_light)
object_info.add_subchunk(ambient_chunk)
if write_keyframe and world.animation_data:
kfdata.add_subchunk(make_ambient_node(world))
''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
# init main key frame data chunk:
kfdata = make_kfdata()
'''
# Make a list of all materials used in the selected meshes (use a dictionary,
# each material is added once)
# Make a list of all materials used in the selected meshes (use dictionary, each material is added once)
materialDict = {}
mesh_objects = []
@ -1237,7 +1534,7 @@ def save(operator,
camera_objects = [ob for ob in objects if ob.type == 'CAMERA']
for ob in objects:
# get derived objects
# Get derived objects
derived_dict = bpy_extras.io_utils.create_derived_objects(depsgraph, [ob])
derived = derived_dict.get(ob)
@ -1260,7 +1557,7 @@ def save(operator,
ma_ls = data.materials
ma_ls_len = len(ma_ls)
# get material/image tuples
# Get material/image tuples
if data.uv_layers:
if not ma_ls:
ma = ma_name = None
@ -1272,7 +1569,7 @@ def save(operator,
ma_index = f.material_index = 0
ma = ma_ls[ma_index]
ma_name = None if ma is None else ma.name
# else there already set to none
# Else there already set to none
img = get_uv_image(ma)
img_name = None if img is None else img.name
@ -1281,7 +1578,7 @@ def save(operator,
else:
for ma in ma_ls:
if ma: # material may be None so check its not
if ma: # Material may be None so check its not
materialDict.setdefault((ma.name, None), (ma, None))
# Why 0 Why!
@ -1296,28 +1593,34 @@ def save(operator,
# Collect translation for transformation matrix
translation = {}
rotation = {}
scale = {}
# Give all objects a unique ID and build a dictionary from object name to object id
# name_to_id = {}
for ob, data, matrix in mesh_objects:
translation[ob.name] = ob.location
rotation[ob.name] = ob.rotation_euler.to_quaternion().inverted()
scale[ob.name] = ob.scale
# name_to_id[ob.name]= len(name_to_id)
for ob in empty_objects:
translation[ob.name] = ob.location
rotation[ob.name] = ob.rotation_euler.to_quaternion().inverted()
scale[ob.name] = ob.scale
# name_to_id[ob.name]= len(name_to_id)
# Create object chunks for all meshes
i = 0
for ob, mesh, matrix in mesh_objects:
# create a new object chunk
# Create a new object chunk
object_chunk = _3ds_chunk(OBJECT)
# set the object name
# Set the object name
object_chunk.add_variable("name", _3ds_string(sane_name(ob.name)))
# make a mesh chunk out of the mesh
# Make a mesh chunk out of the mesh
object_chunk.add_subchunk(make_mesh_chunk(ob, mesh, matrix, materialDict, translation))
# Ensure the mesh has no over sized arrays, skip ones that do!
@ -1327,24 +1630,25 @@ def save(operator,
else:
operator.report({'WARNING'}, "Object %r can't be written into a 3DS file")
''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
# make a kf object node for the object:
kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
'''
# Export kf object node
if write_keyframe:
kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale))
i += i
# Create chunks for all empties
''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
# Create chunks for all empties, only requires a kf object node
if write_keyframe:
for ob in empty_objects:
# Empties only require a kf object node:
kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
pass
'''
kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale))
# Create light object chunks
for ob in light_objects:
object_chunk = _3ds_chunk(OBJECT)
translation[ob.name] = ob.location
rotation[ob.name] = ob.rotation_euler.to_quaternion()
scale[ob.name] = ob.scale
# Add light data subchunks
light_chunk = _3ds_chunk(OBJECT_LIGHT)
color_float_chunk = _3ds_chunk(RGB)
energy_factor = _3ds_chunk(LIGHT_MULTIPLIER)
@ -1381,9 +1685,20 @@ def save(operator,
object_chunk.add_subchunk(light_chunk)
object_info.add_subchunk(object_chunk)
# Export light and spotlight target node
if write_keyframe:
kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale))
if ob.data.type == 'SPOT':
kfdata.add_subchunk(make_target_node(ob, translation, rotation, scale))
# Create camera object chunks
for ob in camera_objects:
object_chunk = _3ds_chunk(OBJECT)
translation[ob.name] = ob.location
rotation[ob.name] = ob.rotation_euler.to_quaternion()
scale[ob.name] = ob.scale
# Add camera data subchunks
camera_chunk = _3ds_chunk(OBJECT_CAMERA)
diagonal = math.copysign(math.sqrt(pow(ob.location[0], 2) + pow(ob.location[1], 2)), ob.location[1])
focus_x = ob.location[0] + (ob.location[1] * math.tan(ob.rotation_euler[2]))
@ -1397,13 +1712,17 @@ def save(operator,
object_chunk.add_subchunk(camera_chunk)
object_info.add_subchunk(object_chunk)
# Export camera and target node
if write_keyframe:
kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale))
kfdata.add_subchunk(make_target_node(ob, translation, rotation, scale))
# Add main object info chunk to primary chunk
primary.add_subchunk(object_info)
''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
# Add main keyframe data chunk to primary chunk:
# Add main keyframe data chunk to primary chunk
if write_keyframe:
primary.add_subchunk(kfdata)
'''
# At this point, the chunk hierarchy is completely built
# Check the size

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, 18),
"version": (4, 0, 0),
'blender': (3, 5, 0),
'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0',

View File

@ -1349,7 +1349,6 @@ class NWMergeNodes(Operator, NWBase):
if tree_type == 'COMPOSITING':
first = 1
second = 2
add.width_hidden = 100.0
elif nodes_list == selected_math:
add_type = node_type + 'Math'
add = nodes.new(add_type)
@ -1359,7 +1358,6 @@ class NWMergeNodes(Operator, NWBase):
loc_y = loc_y - 50
first = 0
second = 1
add.width_hidden = 100.0
elif nodes_list == selected_shader:
if mode == 'MIX':
add_type = node_type + 'MixShader'
@ -1369,7 +1367,6 @@ class NWMergeNodes(Operator, NWBase):
loc_y = loc_y - 50
first = 1
second = 2
add.width_hidden = 100.0
elif mode == 'ADD':
add_type = node_type + 'AddShader'
add = nodes.new(add_type)
@ -1378,7 +1375,6 @@ class NWMergeNodes(Operator, NWBase):
loc_y = loc_y - 50
first = 0
second = 1
add.width_hidden = 100.0
elif nodes_list == selected_geometry:
if mode in ('JOIN', 'MIX'):
add_type = node_type + 'JoinGeometry'
@ -1401,7 +1397,6 @@ class NWMergeNodes(Operator, NWBase):
loc_y = loc_y - 50
first = 0
second = 1
add.width_hidden = 100.0
elif nodes_list == selected_z:
add = nodes.new('CompositorNodeZcombine')
add.show_preview = False
@ -1410,7 +1405,6 @@ class NWMergeNodes(Operator, NWBase):
loc_y = loc_y - 50
first = 0
second = 2
add.width_hidden = 100.0
elif nodes_list == selected_alphaover:
add = nodes.new('CompositorNodeAlphaOver')
add.show_preview = False
@ -1419,7 +1413,6 @@ class NWMergeNodes(Operator, NWBase):
loc_y = loc_y - 50
first = 1
second = 2
add.width_hidden = 100.0
add.location = loc_x, loc_y
loc_y += offset_y
add.select = True
@ -2184,7 +2177,6 @@ class NWAddReroutes(Operator, NWBase):
# unhide 'REROUTE' nodes to avoid issues with location.y
if node.type == 'REROUTE':
node.hide = False
# When node is hidden - width_hidden not usable.
# Hack needed to calculate real width
if node.hide:
bpy.ops.node.select_all(action='DESELECT')
@ -2726,7 +2718,6 @@ class NWAddMultipleImages(Operator, NWBase, ImportHelper):
new_nodes.append(node)
node.label = fname
node.hide = True
node.width_hidden = 100
node.location.x = xloc
node.location.y = yloc
yloc -= 40

View File

@ -308,11 +308,16 @@ def CreateBevel(context, CurrentObject):
bpy.ops.object.mode_set(mode='OBJECT')
CurrentObject.data.use_customdata_edge_bevel = True
bevel_weights = CurrentObject.data.attributes["bevel_weight_edge"]
if not bevel_weights:
bevel_weights = CurrentObject.data.attributes.new("bevel_weight_edge", 'FLOAT', 'EDGE')
if bevel_weights.data_type != 'FLOAT' or bevel_weights.domain != 'EDGE':
bevel_weights = None
for i in range(len(CurrentObject.data.edges)):
if CurrentObject.data.edges[i].select is True:
CurrentObject.data.edges[i].bevel_weight = 1.0
if bevel_weights:
bevel_weights.data[i] = 1.0
CurrentObject.data.edges[i].use_edge_sharp = True
bevel_modifier = False