Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
crapola committed Jan 19, 2023
0 parents commit bfc60a8
Show file tree
Hide file tree
Showing 5 changed files with 795 additions and 0 deletions.
155 changes: 155 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@

bl_info={
"name": "Import and export old Unreal .T3D format",
"author": "Crapola",
"version": (1,0,0),
"blender": (2,80,0),
"location": "File > Import-Export ; Object",
"description": "Import and export UnrealED .T3D files.",
"doc_url":"", # TODO: Link to repo.
"tracker_url":"", # TODO: Link to repo.
"support":"COMMUNITY",
"category":"Import-Export", # Category in Add-ons browser.
}

import bpy

from . import exporter, importer

INVALID_FILENAME="Invalid file name."

class OBJECT_OT_export_t3d_clipboard(bpy.types.Operator):
"""Export selected meshes to T3D into the clipboard."""
bl_idname="object.export_t3d_clipboard"
bl_label="Export T3D to clipboard"

scale:bpy.props.FloatProperty(name="Scale Multiplier",default=128.0)

@classmethod
def poll(cls,context):
return context.selected_objects

def execute(self,context):
sel_objs=[obj for obj in context.selected_objects if obj.type=='MESH']
num_brushes,txt=exporter.export(sel_objs,self.scale)
context.window_manager.clipboard=txt
self.report({'INFO'},f"{num_brushes} brushes exported to clipboard.")
return {'FINISHED'}

def invoke(self, context, event):
wm=context.window_manager
return wm.invoke_props_dialog(self)

class BT3D_MT_file_export(bpy.types.Operator):
"""Export T3D file."""
bl_idname="bt3d.file_export"
bl_label="Export Unreal T3D (.t3d)"
filename:bpy.props.StringProperty(subtype='FILE_NAME')
filepath:bpy.props.StringProperty(subtype='FILE_PATH')
scale:bpy.props.FloatProperty(
name="Scale Multiplier",
default=1.0,
subtype='FACTOR')
def execute(self,context):
if not self.filename.split(".")[0]:
self.report({'ERROR'},INVALID_FILENAME)
return {'CANCELLED'}
# Get meshes in scene.
objs=[obj for obj in context.scene.objects if obj.type=='MESH']
if not objs:
self.report({'WARNING'},"There are no meshes in scene to export.")
return {'CANCELLED'}
num_brushes,txt=exporter.export(objs,self.scale)
self.filepath=bpy.path.ensure_ext(self.filepath,".t3d")
if not num_brushes:
self.report({'WARNING'},"Nothing was converted.")
return {'CANCELLED'}
with open(self.filepath,"w") as f:
f.write(txt)
self.report({'INFO'},f"{num_brushes} brushes saved to {self.filepath}.")
return {'FINISHED'}
def invoke(self, context, event):
if not self.filepath:
self.filepath=bpy.path.ensure_ext(bpy.data.filepath,".t3d")
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}

class BT3D_MT_file_import(bpy.types.Operator):
"""Import T3D file."""
bl_idname="bt3d.file_import"
bl_label="Import Unreal T3D (.t3d)"

filename:bpy.props.StringProperty(
name="input filename",
subtype='FILE_NAME'
)
filepath:bpy.props.StringProperty(
name="input file",
subtype='FILE_PATH'
)
filter_glob:bpy.props.StringProperty(
default="*.t3d",
options={'HIDDEN'},
)
# Options.
flip:bpy.props.BoolProperty(
name="Flip normals",
description="Flip normals of CSG_Subtract brushes",
default=False
)
snap_vertices:bpy.props.BoolProperty(
name="Snap vertices",
description="Snap to grid.",
default=False
)
snap_distance:bpy.props.FloatProperty(
name="Snap distance",
default=1.0
)

def execute(self,context):
if not self.filename.split(".")[0]:
self.report({'ERROR'},INVALID_FILENAME)
return {'CANCELLED'}
importer.import_t3d_file(
context,
self.filepath,
self.filename,
self.snap_vertices,
self.snap_distance,
self.flip)
return {'FINISHED'}

def invoke(self, context, event):
wm=context.window_manager
wm.fileselect_add(self)
return {'RUNNING_MODAL'}

classes = (
BT3D_MT_file_export,
BT3D_MT_file_import,
OBJECT_OT_export_t3d_clipboard,
)
register_classes, unregister_classes = bpy.utils.register_classes_factory(classes)

menus=(
lambda x,_:x.layout.operator(OBJECT_OT_export_t3d_clipboard.bl_idname),
lambda x,_:x.layout.operator(BT3D_MT_file_export.bl_idname),
lambda x,_:x.layout.operator(BT3D_MT_file_import.bl_idname),
)

def register():
print("Registering.")
register_classes()
# Add to menu.
bpy.types.VIEW3D_MT_object.append(menus[0])
bpy.types.TOPBAR_MT_file_export.append(menus[1])
bpy.types.TOPBAR_MT_file_import.append(menus[2])

def unregister():
print("Unregistering.")
# Remove from menu.
bpy.types.VIEW3D_MT_object.remove(menus[0])
bpy.types.TOPBAR_MT_file_export.remove(menus[1])
bpy.types.TOPBAR_MT_file_import.remove(menus[2])
unregister_classes()
117 changes: 117 additions & 0 deletions exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import math

import bmesh
import bpy
from mathutils import Euler, Matrix, Vector

try:
from .t3d import Brush, Polygon, Vertex
except:
from t3d import Brush, Polygon, Vertex

DEBUG=0
def _print(*_):pass
if DEBUG:_print=print

TEXTURE_SIZE=256

def basis_from_points(points:list)->Matrix:
""" 2D Basis, middle point is origin. """
m=Matrix( ((1,0,0),(0,1,0),(0,0,1)) )
v0,v1,v2=points
m[0].xy=(v2-v1) #X
m[1].xy=(v0-v1) #Y
m[2].xy=v1 # Origin
m.transpose()
return m

def export(object_list,scale_multiplier=128.0)->tuple:
stuff=""
for obj in object_list:
mesh:bpy.types.Mesh=obj.data
bm=bmesh.new()
bm.from_mesh(mesh)

uv_layer_0=None
if bm.loops.layers.uv:
uv_layer_0: bmesh.types.BMLayerItem = bm.loops.layers.uv[0]
layer_texture=bm.faces.layers.string.get("texture")
poly_list=[]
for f in bm.faces:
# Vertices.
verts=[Vertex(v.co*scale_multiplier) for v in f.verts]
poly=Polygon(verts,f.normal)

# Get texture name, either from custom attribute if it exists (brush
# was imported), or material.
if layer_texture:
poly.texture=f[layer_texture].decode('utf-8')
elif len(obj.data.materials)>0:
name=obj.data.materials[f.material_index].name
poly.texture=name

_print(f"---- Face {poly.texture} ----")

# Texture coordinates.

# Compute floor/UV (plane where Z is 0) to surface transform, with
# the first three verts of polygon.
n=f.normal
first_three=f.loops[0:3]
v0,v1,v2=[v.vert.co for v in first_three]
b=Matrix()
b[2].xyz=n
b[0].xyz=(v0-v1)
b[1].xyz=(v2-v1)
b[3].xyz=v0
b.transpose() # Needed.
b.invert()
axis_x=Vector((1,0,0))
axis_y=Vector((0,1,0))
# Basic texturing.
poly.u=b.transposed()@axis_x
poly.v=b.transposed()@axis_y

# Convert Blender UV Map if it exists.
if uv_layer_0:
v0i=(b@v0)
v1i=(b@v1)
v2i=(b@v2)
# We assume UVs are a linear transform of the polygon shape.
# Figure out that transform by using the first three verts.
first_three_uvs=[l[uv_layer_0].uv for l in f.loops[0:3]]
u0,u1,u2=first_three_uvs
# mv=quad->poly, mu=poly->uv.
mv=basis_from_points( (v0i.xy,v1i.xy,v2i.xy) )
mu=basis_from_points( (u0,u1,u2) )
t=mu @ mv.inverted()
t.resize_4x4()
t.transpose()
b.transpose()

poly.u=b @ (t @ axis_x*TEXTURE_SIZE/scale_multiplier)
poly.v=b @ (t @ axis_y*TEXTURE_SIZE/scale_multiplier)

# Pan.
pan=mu[1]*TEXTURE_SIZE
if pan:
poly.pan=(int(pan.x),int(pan.y))

poly_list.append(poly)

brush=Brush(poly_list,obj.location*scale_multiplier,obj.name)

if obj.rotation_euler!=Euler((0,0,0)):
brush.rotation=Vector(obj.rotation_euler)*65536/math.tau
if obj.scale!=Vector((0,0,0)):
brush.mainscale=obj.scale
if obj.get("csg"):
brush.csg=obj["csg"]

stuff+=str(brush)

bm.to_mesh(mesh)
bm.free()

everything=f"""Begin Map\n{stuff}End Map\n"""
return len(object_list),everything
Loading

0 comments on commit bfc60a8

Please sign in to comment.