Skip to content

Commit

Permalink
Merge pull request #2 from haekb/features/animations-and-2.80-support
Browse files Browse the repository at this point in the history
[WIP] Features/animations and 2.80 support
  • Loading branch information
cmbasnett authored Dec 31, 2019
2 parents 55a80c3 + 3bac83b commit 740d3d4
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 44 deletions.
29 changes: 20 additions & 9 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
bl_info = {
'name': 'Lithtech ABC Format',
'description': 'Import and export ABC models and animations files from Lithtech 2.1 games (eg. No One Lives Forever).',
'author': 'Colin Basnett',
'version': (1, 0, 0),
'blender': (2, 79, 0),
'author': 'Colin Basnett and HeyJake',
'version': (1, 1, 0),
'blender': (2, 80, 0),
'location': 'File > Import-Export',
'warning': 'This add-on is under development.',
'wiki_url': 'https://github.com/cmbasnett/io_scene_abc/wiki',
Expand Down Expand Up @@ -33,14 +33,25 @@
from . import importer
from . import exporter

from bpy.utils import register_class, unregister_class

classes = (
importer.ImportOperator,
exporter.ExportOperator
)

def register():
bpy.utils.register_module(__name__)
bpy.types.INFO_MT_file_import.append(importer.ImportOperator.menu_func_import)
bpy.types.INFO_MT_file_export.append(exporter.ExportOperator.menu_func_export)
for cls in classes:
register_class(cls)
# bpy.utils.register_module(__name__)
bpy.types.TOPBAR_MT_file_import.append(importer.ImportOperator.menu_func_import)
bpy.types.TOPBAR_MT_file_export.append(exporter.ExportOperator.menu_func_export)


def unregister():
bpy.utils.unregister_module(__name__)
bpy.types.INFO_MT_file_import.remove(importer.ImportOperator.menu_func_import)
bpy.types.INFO_MT_file_export.remove(exporter.ExportOperator.menu_func_export)
for cls in reversed(classes):
unregister_class(cls)
#bpy.utils.unregister_module(__name__)
bpy.types.TOPBAR_MT_file_import.remove(importer.ImportOperator.menu_func_import)
bpy.types.TOPBAR_MT_file_export.remove(exporter.ExportOperator.menu_func_export)

183 changes: 148 additions & 35 deletions src/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import bmesh
import os
import math
from mathutils import Vector, Matrix, Quaternion
from math import pi
from mathutils import Vector, Matrix, Quaternion, Euler
from bpy.props import StringProperty, BoolProperty, FloatProperty
from .dtx import DTX
from .reader import ModelReader
Expand All @@ -21,20 +22,35 @@ def __init__(self):


def import_model(model, options):
utils.delete_all_objects()
# utils.delete_all_objects()

# This seems to be the rage these days
Context = bpy.context
Data = bpy.data
Ops = bpy.ops

# Create our new collection. This will help us later on..
collection = Data.collections.new(model.name)
# Add our collection to the scene
Context.scene.collection.children.link(collection)

# TODO: clear the orphan list for ultra name purity
bpy.ops.object.lamp_add(type='SUN')
sun = Data.lights.new(name="Sun", type='SUN')
sun_object = Data.objects.new("Sun", sun)
collection.objects.link(sun_object)

# Create the armature
armature = bpy.data.armatures.new(model.name)
armature_object = bpy.data.objects.new(model.name, armature)
armature_object.data.draw_type = 'STICK'
armature_object.show_x_ray = True
armature_object.data.display_type = 'STICK'
armature_object.show_in_front = True

collection.objects.link(armature_object)
armature_object.select_set(True)
#armature_object.edit_bones()

bpy.context.scene.objects.link(armature_object)
bpy.context.scene.objects.active = armature_object
bpy.ops.object.mode_set(mode='EDIT')
Context.view_layer.objects.active = armature_object
Ops.object.mode_set(mode='EDIT')

for node in model.nodes:
bone = armature.edit_bones.new(node.name)
Expand Down Expand Up @@ -113,44 +129,54 @@ def import_model(model, options):

# TODO: Now calculate the roll of the bone.

bpy.ops.object.mode_set(mode='OBJECT')
Ops.object.mode_set(mode='OBJECT')

''' Add sockets as empties with a child-of constraint to the appropriate bone. '''
if options.should_import_sockets:
for socket in model.sockets:
empty_object = bpy.data.objects.new(socket.name, None)
empty_object = Data.objects.new(socket.name, None)
empty_object.location = socket.location
empty_object.rotation_quaternion = socket.rotation
empty_object.empty_draw_type = 'PLAIN_AXES'
empty_object.empty_display_type = 'PLAIN_AXES'
child_of_constraint = empty_object.constraints.new('CHILD_OF')
child_of_constraint.target = armature_object
child_of_constraint.subtarget = model.nodes[socket.node_index].name
empty_object.parent = armature_object
bpy.context.scene.objects.link(empty_object)
collection.objects.link(empty_object)

''' Determine the amount of LODs to import. '''
lod_import_count = model.lod_count if options.should_import_lods else 1

''' Create materials. '''
materials = []

for i, piece in enumerate(model.pieces):
while len(materials) <= piece.material_index:
''' Create a material for the new piece. '''
material = bpy.data.materials.new(piece.name)
material.diffuse_intensity = 1.0
material = Data.materials.new(piece.name)
material.specular_intensity = piece.specular_power / 100

material.use_nodes = True

# TODO: maybe put something in here for the specular scale?
materials.append(material)

''' Create texture. '''
texture = bpy.data.textures.new(piece.name, type='IMAGE')

# Swapped over to nodes
bsdf = material.node_tree.nodes["Principled BSDF"]
texImage = material.node_tree.nodes.new('ShaderNodeTexImage')
material.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color'])

texture = Data.textures.new(piece.name, type='IMAGE')

# Note: Texture image names are stored in ModelButes.txt
if options.image is not None:
texture.image = bpy.data.images.new('okay', width=options.image.width, height=options.image.height, alpha=True) # TODO: get real name
texture.image = bpy.data.images.new(piece.name, width=options.image.width, height=options.image.height, alpha=True) # TODO: get real name
texture.image.pixels[:] = options.image.pixels[:]

texture_slot = material.texture_slots.add()
texture_slot.texture = texture
texImage.image = texture.image


''' Create a mesh for each piece of each LOD level that we are importing. '''
for lod_index in range(lod_import_count):
Expand All @@ -161,19 +187,24 @@ def import_model(model, options):
mesh_name = piece.name
if options.should_import_lods:
mesh_name += '.LOD{0!s}'.format(lod_index)
mesh = bpy.data.meshes.new(model.name)
mesh_object = bpy.data.objects.new(mesh_name, mesh)
mesh = Data.meshes.new(model.name)
mesh_object = Data.objects.new(mesh_name, mesh)

''' Add materials to mesh. '''
for material in materials:
''' Create UV map. '''
'''
uv_texture = mesh.uv_textures.new()
mesh.materials.append(material)
material.texture_slots[0].uv_layer = uv_texture.name
'''
uv_texture = mesh.uv_layers.new()
mesh.materials.append(material)
# material.uv_layers[0].name = uv_texture.name

''' Create a vertex group for each node. '''
for node in model.nodes:
mesh_object.vertex_groups.new(node.name)
mesh_object.vertex_groups.new(name=node.name)

# TODO: these need to be reset for each mesh
vertex_offset = 0
Expand Down Expand Up @@ -228,8 +259,11 @@ def import_model(model, options):
Assign texture coordinates.
'''
material_face_offsets = [0] * len(mesh.materials)

uv_texture = mesh.uv_layers[piece.material_index]

# Set the correct UV as active
uv_texture.active_render = True

for face_index, face in enumerate(lod.faces):
material_face_offset = material_face_offsets[0] # TODO: is this right?
texcoords = [vertex.texcoord for vertex in face.vertices]
Expand All @@ -252,11 +286,12 @@ def import_model(model, options):
mesh.validate(clean_customdata=False)
mesh.update(calc_edges=False)

bpy.context.scene.objects.link(mesh_object)
# add it to our collection c:
collection.objects.link(mesh_object)

if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
if Ops.object.mode_set.poll():
Ops.object.mode_set(mode='OBJECT')
Ops.object.select_all(action='DESELECT')

''' Add an armature modifier. '''
armature_modifier = mesh_object.modifiers.new(name='Armature', type='ARMATURE')
Expand Down Expand Up @@ -285,23 +320,90 @@ def import_model(model, options):

actions = []

index = 0
for animation in model.animations:
print("Processing ", animation.name)
if(index > 0):
break

index = index + 1
# Create a new action with the animation name
action = bpy.data.actions.new(name=animation.name)

# Temp set
armature_object.animation_data.action = action

# For every keyframe
for keyframe_index, keyframe in enumerate(animation.keyframes):
bpy.context.scene.frame_set(keyframe.time)
for node_index, (pose_bone, bone, node) in enumerate(zip(armature_object.pose.bones, armature.bones, model.nodes)):
assert(pose_bone.name == node.name)
# Set keyframe time - Scale it down because it's way too slow for testing
Context.scene.frame_set(keyframe.time * 0.01)

'''
Recursively apply transformations to a nodes children
Notes: It carries everything (nodes, pose_bones..) with it, because I expected it to not be a child of this scope...oops!
'''
def recursively_apply_transform(nodes, node_index, pose_bones, parent_matrix):
# keyframe_index = 0
node = nodes[node_index]
pose_bone = pose_bones[node_index]
original_index = node_index

#print("[",node_index,"] Applying transform to : ", node.name)


# Get the current transform
transform = animation.node_keyframe_transforms[node_index][keyframe_index]

# Correct-ish
# rotation = Quaternion( (transform.rotation.w, -transform.rotation.z, -transform.rotation.x, transform.rotation.y) )


rotation = Quaternion( (transform.rotation.w, -transform.rotation.z, -transform.rotation.x, transform.rotation.y) )

matrix = rotation.to_matrix().to_4x4() # transform.rotation.to_matrix().to_4x4()

# Apply the translation
matrix.Translation(transform.location.xzy)


# Use a matrix instead!
pose_bone.matrix = parent_matrix @ matrix

# Recursively apply our transform to our children!
# print("[",node_index,"] Found children count : ", node.child_count)

#if (node.child_count):
# print("[",original_index,"] Recurse Start --- ",node.name)

for index in range(0, node.child_count):
node_index = node_index + 1
#print("[",original_index,"] Applying transform to child : ", nodes[node_index].name)

node_index = recursively_apply_transform(nodes, node_index, pose_bones, pose_bone.matrix)

#if (node.child_count):
# print("[",original_index,"] Recurse End --- ",node.name)

return node_index
'''
Func End
'''

recursively_apply_transform(model.nodes, 0, armature_object.pose.bones, Matrix())

# For every bone
for bone, node in zip(armature_object.pose.bones, model.nodes):
bone.keyframe_insert('location')
bone.keyframe_insert('rotation_quaternion')

# Add to actions array
actions.append(action)

# Add our actions to animation data
armature_object.animation_data.action = actions[0]

bpy.context.scene.frame_set(0)
# Set our keyframe time to 0
Context.scene.frame_set(0)

# TODO: make an option to convert to blender coordinate system
# armature_object.rotation_euler.x = math.radians(90)
Expand Down Expand Up @@ -365,28 +467,38 @@ class ImportOperator(bpy.types.Operator, bpy_extras.io_utils.ImportHelper):
default=False,
)

should_clear_scene = BoolProperty(
name="Clear Scene",
description="When checked, the scene will be cleared before the model is imported.",
default=False,
)

def draw(self, context):
layout = self.layout

box = layout.box()
box.label("Nodes")
box.label(text='Nodes')
box.row().prop(self, 'bone_length_min')
box.row().prop(self, 'should_import_sockets')

box = layout.box()
box.label('Meshes')
box.label(text='Meshes')
box.row().prop(self, 'should_import_lods')
box.row().prop(self, 'should_merge_pieces')

box = layout.box()
box.label('Materials')
box.label(text='Materials')
box.row().prop(self, 'should_import_textures')
box.row().prop(self, 'should_assign_materials')
# box.row().prop(self, 'should_assign_materials')

box = layout.box()
box.label('Animations')
box.label(text='Animations')
box.row().prop(self, 'should_import_animations')

box = layout.box()
box.label(text='Misc')
box.row().prop(self, 'should_clear_scene')

def execute(self, context):
# Load the model
model = ModelReader().from_file(self.filepath)
Expand All @@ -406,6 +518,7 @@ def execute(self, context):
options.should_import_animations = self.should_import_animations
options.should_import_sockets = self.should_import_sockets
options.should_merge_pieces = self.should_merge_pieces
options.should_clear_scene = self.should_clear_scene
options.image = image
import_model(model, options)
return {'FINISHED'}
Expand Down

0 comments on commit 740d3d4

Please sign in to comment.