Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic node import #19

Merged
merged 37 commits into from
Jan 12, 2020
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
897410e
Add shape creation functions.
ShellyHerself Aug 16, 2019
df17d72
Fix a function name.
ShellyHerself Aug 16, 2019
0b8ee7d
Stubbed out some Halo <-> scene interaction classes.
ShellyHerself Aug 16, 2019
528e555
Simple in theory. Complicated in practice. Delete halo1/types
ShellyHerself Aug 18, 2019
6bc3358
Floats, not ints.
ShellyHerself Aug 18, 2019
90d210e
Merge branch 'develop' into nodes
ShellyHerself Aug 19, 2019
c95d858
Updated whitespace.
ShellyHerself Aug 19, 2019
6deb743
Fix the weird module importing stuff.
ShellyHerself Aug 29, 2019
e2502f6
Set up all UI elements for simple node importing.
ShellyHerself Aug 29, 2019
fee15c5
Merge branch 'develop' into nodes
ShellyHerself Aug 29, 2019
3e0d121
Undo import thing
ShellyHerself Aug 29, 2019
657cf2b
Update a class docstring.
ShellyHerself Aug 29, 2019
33fdd1d
Add untested halo1 model reading function.
ShellyHerself Aug 30, 2019
7c21668
Fixed import_halo1model to actually work
ShellyHerself Aug 30, 2019
2d362cc
Fix jms import.
ShellyHerself Aug 30, 2019
c1dde58
Import nodes
ShellyHerself Aug 30, 2019
6206d7c
Fix comment.
ShellyHerself Aug 30, 2019
c34daff
Fix import workaround (Thanks Moses)
ShellyHerself Aug 30, 2019
c50ecf1
Finalize node import function for now.
ShellyHerself Aug 30, 2019
feeb792
Make import store the return value of import_halo1_nodes()
ShellyHerself Aug 31, 2019
9c0c30c
Fix scaling.
ShellyHerself Sep 1, 2019
8fff147
Invert W component when importing nodes
ShellyHerself Dec 8, 2019
03a6129
Use scale_enum
ShellyHerself Dec 8, 2019
6e108b1
Remove the need for the copied code segment from Mozzarilla
ShellyHerself Dec 8, 2019
fed75d2
Remove debug prints
ShellyHerself Dec 8, 2019
9011895
Add node_size property and minimum values for sizes
ShellyHerself Dec 8, 2019
e40ac5b
Improve error for non h1 jms. Case-insensitively check file extension
ShellyHerself Dec 8, 2019
b66d331
Correct docstring
ShellyHerself Dec 8, 2019
194361b
Add a TODO
ShellyHerself Dec 8, 2019
46d61b9
Make modules register through a list.
ShellyHerself Dec 10, 2019
80292de
Move scale enum values to constants file
ShellyHerself Dec 31, 2019
b57eebd
Return imported nodes as dict instead of list
ShellyHerself Dec 31, 2019
45f0c4b
Remove todo
ShellyHerself Dec 31, 2019
144bf20
Use enumerate
ShellyHerself Jan 4, 2020
2c9bfcf
Use JmsModel scale as base scale
ShellyHerself Jan 10, 2020
40c2d12
Add constants for JmsModel versions
ShellyHerself Jan 10, 2020
506b4b0
Improve unregister comment
ShellyHerself Jan 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,34 @@
"category": "Import-Export"
}

# Initialize the libraries module.
from . import lib

# Import all submodules.
from .menu import topbar_dropdown
from .menu.import_export import halo1_model

modules = [
halo1_model,
topbar_dropdown,
]

def register():
'''
Registers classes on load by calling the
register functions in their respective modules.
'''
topbar_dropdown.register()
for module in modules:
module.register()

def unregister():
'''
Unregisters classes on unload by calling the
unregister functions in their respective modules.
'''
topbar_dropdown.unregister()
#Unregister classes in reverse order to avoid any dependency problems.
for module in reverse(modules):
module.unregister()

if __name__ == "__main__":
register()
13 changes: 13 additions & 0 deletions constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

# These are used to figure out what amount something needs
# to be scaled when importing or exporting.
SCALE_MULTIPLIERS = {
# 10 : A world unit is 10 feet.
# 0.032808 : feet per centimeter
# 100.0 : centimeters per meter.
'METRIC' : 10.0/0.032808/100.0,
# Halo models in 3dsmax as 100x as big as internally.
'MAX' : 100.0,
# Base scale. Used for straight conversions.
'HALO' : 1.0,
}
Empty file added halo1/__init__.py
Empty file.
76 changes: 76 additions & 0 deletions halo1/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import bpy

from reclaimer.hek.defs.mode import mode_def
from reclaimer.hek.defs.mod2 import mod2_def
from reclaimer.model.jms import read_jms
from reclaimer.model.model_decompilation import extract_model

from ..scene.shapes import create_sphere

def read_halo1model(filepath):
'''Takes a halo1 model file and turns it into a jms object.'''

# TODO: Use a tag handler to see if these files actually are what they
# say they are. We can get really nasty parsing problems if they aren't.

# These two model types can be imported the same way because of their
# nearly matching structures when built into a python object.
if (filepath.lower().endswith('.gbxmodel')
or filepath.lower().endswith('.model')):
# Load model
tag = None
if filepath.lower().endswith('.gbxmodel'):
tag = mod2_def.build(filepath=filepath)
else:
tag = mode_def.build(filepath=filepath)
#TODO: Get all lod permutations.
#Only getting the superhigh perms for now
jms = extract_model(tag.data.tagdata, write_jms=False)[0]
return jms

if filepath.lower().endswith('.jms'):
# Read jms file into string.
jms_string = ""
with open(filepath, 'r') as jms_file:
jms_string = jms_file.read()
# Read Jms data from string.
jms = read_jms(jms_string)
# Make sure it's a Halo 1 jms
if jms.version != "8200":
raise ValueError('Not a Halo 1 jms!')

return jms

def import_halo1_nodes(jms, scale=1.0, node_size=0.02):
'''
Import all the nodes from a jms into the scene and returns a list of them.
'''
# Jms are scaled at x100 halo. Undo that.
scale = scale/100

scene_nodes = dict()
for i, node in enumerate(jms.nodes):
scene_node = create_sphere(name="@"+node.name, size=node_size)

# Assign parent if index is valid.
if node.parent_index in range(len(scene_nodes)):
scene_node.parent = scene_nodes[node.parent_index]

# Store original rotation mode.
rot_mode = scene_node.rotation_mode
# Set rotation mode to quaternion and apply the rotation.
scene_node.rotation_mode = 'QUATERNION'
scene_node.rotation_quaternion = (
-node.rot_w, node.rot_i, node.rot_j, node.rot_k
)
# Set rotation mode back.
scene_node.rotation_mode = rot_mode

# Undo 100x scaling from jms files.
scene_node.location = (
node.pos_x*scale, node.pos_y*scale, node.pos_z*scale
)

scene_nodes[i] = scene_node

return scene_nodes
Empty file added menu/import_export/__init__.py
Empty file.
124 changes: 124 additions & 0 deletions menu/import_export/halo1_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import bpy
from bpy.utils import register_class, unregister_class
from bpy.props import BoolProperty, FloatProperty, StringProperty, EnumProperty
from bpy_extras.io_utils import ImportHelper, ExportHelper, orientation_helper, path_reference_mode, axis_conversion

from ...halo1.model import read_halo1model, import_halo1_nodes
from ...constants import SCALE_MULTIPLIERS

#@orientation_helper(axis_forward='-Z') Find the right value for this.
class MT_krieg_ImportHalo1Model(bpy.types.Operator, ImportHelper):
"""
The import operator for gbxmodel/jms models.
This stores the properties when inside of the import dialog, and
it specifies what to show on the side panel when importing.
"""
bl_idname = "import_scene.halo1_model"
bl_label = "Import Halo 1 Model"
bl_options = {'PRESET', 'UNDO'}

# Import-file-dialog settings:

filename_ext = ".gbxmodel"
filter_glob: StringProperty(
default="*.gbxmodel;*.model;*.jms",
options={'HIDDEN'},
)

# Node settings:

use_nodes: BoolProperty(
name="Import Nodes",
description="Import the nodes/frames.",
default=True,
)
node_size: FloatProperty(
name="Node Scene Size",
description="Set the size that nodes should have in the Blender scene if at 1.0 scale.",
default=0.1,
min=0.0,
)
use_armatures: BoolProperty(
name="Import Nodes as Armature",
description="Import the nodes as armature.",
default=False,
)

# Scaling settings:

scale_enum: EnumProperty(
name="Scale",
items=(
('METRIC', "Blender", "Use Blender's metric scaling."),
('MAX', "3ds Max", "Use 3dsmax's 100xHalo scale."),
('HALO', "Internal", "Use Halo's internal 1.0 scale (small)."),
('CUSTOM', "Custom", "Set your own scaling multiplier."),
)
)
scale_float: FloatProperty(
name="Custom Scale",
description="Set your own scale.",
default=1.0,
min=0.0,
)

def execute(self, context):
# Set appropriate scaling
scale = 1.0
if self.scale_enum in SCALE_MULTIPLIERS:
scale = SCALE_MULTIPLIERS[self.scale_enum]
elif self.scale_enum == 'CUSTOM':
scale = self.scale_float
else:
raise ValueError('Invalid scale_enum state.')

# Test if jms import function doesn't crash.
jms = read_halo1model(self.filepath)

# Import nodes into the scene.
nodes = import_halo1_nodes(jms, scale=scale, node_size=self.node_size)

return {'FINISHED'}

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

# Node settings elements:

box = layout.box()
row = box.row()
row.label(text="Nodes:")
row.prop(self, "use_nodes")
if self.use_nodes:
box.prop(self, "node_size")
box.prop(self, "use_armatures")

# Scale settings elements:

box = layout.box()
box.label(text="Scale:")
row = box.row()
row.prop(self, "scale_enum", expand=True)

if self.scale_enum == 'CUSTOM':
row = box.row()
row.prop(self, "scale_float")


# Enumerate all classes for easy register/unregister.
classes = (
MT_krieg_ImportHalo1Model,
)

def register():
for cls in classes:
register_class(cls)


def unregister():
for cls in reversed(classes): #Reversed because: first in, last out.
unregister_class(cls)


if __name__ == "__main__":
register()
7 changes: 7 additions & 0 deletions menu/topbar_dropdown.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from bpy.utils import register_class, unregister_class
from bpy.types import Menu, TOPBAR_MT_editor_menus

from .import_export import halo1_model

class TOPBAR_MT_krieg(Menu):
bl_idname = "TOPBAR_MT_krieg_ext"
bl_label = "Krieg"
Expand Down Expand Up @@ -29,6 +31,11 @@ def draw(self, context):

# Halo 1:

layout.operator(
halo1_model.MT_krieg_ImportHalo1Model.bl_idname,
text="Halo 1 Model (.gbxmodel, .model, .jms)"
)

layout.separator()

# Whatever else:
Expand Down
Empty file added scene/__init__.py
Empty file.
45 changes: 45 additions & 0 deletions scene/shapes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import bpy
import bmesh

def create_sphere(name="new_sphere", size=1.0):
'''
Creates a sphere with the given size.
'''
scene = bpy.context.collection

mesh = bpy.data.meshes.new(name)
sphere = bpy.data.objects.new(name, mesh)

scene.objects.link(sphere)

bm = bmesh.new()
bmesh.ops.create_uvsphere(bm,
u_segments=16, v_segments=16,
diameter=size
)
bm.to_mesh(mesh)
bm.free()

return sphere

def create_cone(name="new_cone", base_size=1.0, height=3.0):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only difference between create_sphere and create_cone is the calls bmesh.ops.create_uvsphere and bmesh.ops.create_cone. Can we do something to reduce the boilerplate required for any new shape?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but I'd like to get the importing done at a basic level before I go in and do good stuff for shapes. I want to get models in before I worry about those details.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to move onto markers, and then geometry before I worry too much about the shapes of the nodes and markers

'''
Creates a cone with the given sizes.
'''
scene = bpy.context.collection

mesh = bpy.data.meshes.new(name)
cone = bpy.data.objects.new(name, mesh)

scene.objects.link(cone)

bm = bmesh.new()
bmesh.ops.create_cone(bm,
segments=16,
diameter1=base_size, diameter2=base_size,
depth=height
)
bm.to_mesh(mesh)
bm.free()

return cone