Skip to content

Commit

Permalink
Merge pull request #10 from crapola/v110
Browse files Browse the repository at this point in the history
V110
  • Loading branch information
crapola authored Feb 18, 2024
2 parents 1d7b442 + 607ecb1 commit 7f77cfd
Show file tree
Hide file tree
Showing 34 changed files with 4,373 additions and 1,949 deletions.
16 changes: 8 additions & 8 deletions blender_t3d/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
bl_info={
"name": "Import and export old Unreal .T3D format",
"author": "Crapola",
"version": (1,0,2),
"version": (1,1,0),
"blender": (3,3,0),
"location": "File > Import-Export ; Object",
"description": "Import and export UnrealED .T3D files.",
Expand All @@ -29,17 +29,17 @@ class OBJECT_OT_export_t3d_clipboard(bpy.types.Operator):
bl_idname:str="object.export_t3d_clipboard"
bl_label:str="Export T3D to clipboard"

scale:bpy.props.FloatProperty(name="Scale Multiplier",default=128.0)
scale:bpy.props.FloatProperty(name="Scale Multiplier",default=1.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)
txt=exporter.export(sel_objs,self.scale)
context.window_manager.clipboard=txt
self.report({'INFO'},f"{num_brushes} brushes exported to clipboard.")
self.report({'INFO'},f"{len(sel_objs)} brushes exported to clipboard.")
return {'FINISHED'}

def invoke(self, context, event):
Expand All @@ -65,14 +65,14 @@ def execute(self,context):
if not objs:
self.report({'WARNING'},"There are no meshes in scene to export.")
return {'CANCELLED'}
num_brushes,txt=exporter.export(objs,self.scale)
txt=exporter.export(objs,self.scale)
self.filepath=bpy.path.ensure_ext(self.filepath,".t3d")
if not num_brushes:
if not txt:
self.report({'WARNING'},"Nothing was converted.")
return {'CANCELLED'}
with open(self.filepath,"w") as f:
with open(self.filepath,"w",encoding="utf-8") as f:
f.write(txt)
self.report({'INFO'},f"{num_brushes} brushes saved to {self.filepath}.")
self.report({'INFO'},f"{len(objs)} brushes saved to {self.filepath}.")
return {'FINISHED'}
def invoke(self, context, event):
if not self.filepath:
Expand Down
266 changes: 146 additions & 120 deletions blender_t3d/exporter.py
Original file line number Diff line number Diff line change
@@ -1,129 +1,155 @@
"""
Exporter.
"""
import math

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

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

DEBUG=1
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:float=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:bmesh.types.BMLayerItem|None=None
if bm.loops.layers.uv:
uv_layer_0 = bm.loops.layers.uv[0]
layer_texture=bm.faces.layers.string.get("texture")
poly_list=[]
for f in bm.faces:
# Vertices.
verts:list[Vertex]=[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 {f.index} {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)

poly.u=-poly.u
poly.v=-poly.v

# Pan.
v=Vector((1,1,1))
pan=mu.transposed()[2]*TEXTURE_SIZE
#pan=(v-mu.transposed()[2] ) *TEXTURE_SIZE
#pan.xy=pan.yz
#pan+=Vector((0,128,0))
_print(mu)
_print(pan)

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
print(obj.keys())
if mesh.get("csg"):
_print("CSG=",mesh["csg"])
brush.csg=mesh["csg"]

stuff+=str(brush)

bm.to_mesh(mesh)
bm.free()

everything=f"""Begin Map\n{stuff}End Map\n"""
return len(object_list),everything
DEBUG=0
def _print(*_):
pass
if DEBUG:
_print=print

TEXTURE_SIZE:float=256.0

def brush_from_object(o:'bpy.types.Object',scale_multiplier:float=1.0)->Brush|str:
""" Turn Blender Object into t3d.Brush. """

if o.type!="MESH":
print(f"{o} is not a mesh.")
return ""

print(f"Exporting {o.name}...")

bm:bmesh.types.BMesh=bmesh.new()
bm.from_mesh(o.data)

poly_list:list[Polygon]=[]
f:bmesh.types.BMFace
for f in bm.faces:
vertices:list[bmesh.types.BMVert]=[v for v in f.verts if isinstance(v,bmesh.types.BMVert)]
verts:list[Vertex]=[Vertex((Vector(v.co)*scale_multiplier).to_tuple()) for v in vertices]
poly=Polygon(verts)
# Texture name.
poly.texture=get_material_name(o,f.material_index)
# Texture coordinates.
poly.origin,poly.u,poly.v=polygon_texture_transform(f,bm)
# Add to the list.
poly_list.append(poly)
bm.to_mesh(o.data)
bm.free()

# Instance Brush with location and name.
brush=Brush(poly_list,o.location*scale_multiplier)
brush.actor_name=o.name.replace(" ","_")

# Rotation and scaling.
if o.rotation_euler!=Euler((0,0,0)):
brush.rotation=Vector(o.rotation_euler)*65536/math.tau
brush.rotation.xy=-brush.rotation.xy
if o.scale!=Vector((0,0,0)):
brush.mainscale=o.scale

# Custom properties.
#print(o.keys())
brush.csg=o.get("csg",brush.csg)
brush.group=o.get("group",brush.group)
brush.polyflags=o.get("polyflags",brush.polyflags)

return brush

def export(object_list,scale_multiplier:float=1.0)->str:
"""
Export objects to a T3D text.
Return empty string if nothing was exported.
"""
# TODO: In a .T3D file, the first brush is the red brush.
# Perhaps insert dummy red brush for file export.
t3d_text:str="".join(str(brush_from_object(obj,scale_multiplier)) for obj in object_list)
if t3d_text:
t3d_text=f"""Begin Map\n{t3d_text}End Map\n"""
return t3d_text

def export_uv(verts:list[Vector],uvs:list[Vector],normal:Vector)->tuple:
""" Return Origin,TextureU,TextureV in tuple. """
uvs=[Vector((uv.x,1-uv.y))*TEXTURE_SIZE for uv in uvs]
verts=rotate_triangle_towards_normal(verts,Vector((0,0,1)))
_print("Rotated verts to XY plane:",verts)
height=verts[0].z
verts=[v.xy for v in verts]
_print("Flat Verts:",verts)
m_uvs=Matrix((*[v.to_3d()+Vector((0,0,1)) for v in uvs],))
m_verts=Matrix((*[v.to_3d()+Vector((0,0,1)) for v in verts],))
_print("m_uvs:",m_uvs)
_print("m_verts:",m_verts)
m_uvs_inverse:Matrix=m_uvs.inverted_safe()
m_verts_inverse:Matrix=m_verts.inverted_safe()
_print("m_verts_inverse:",m_verts_inverse)
_print("m_uvs_inverse:",m_uvs_inverse)
u2v:Matrix=m_verts_inverse@m_uvs
_print("Result UVs to Verts transform:",u2v)
rot:Matrix=normal_rotation(Vector((0,0,1)),normal)
tu:Vector=u2v.col[0].xy
tv:Vector=u2v.col[1].xy
o:Vector=u2v[2].xy
tu=rot@tu.to_3d()
tv=rot@tv.to_3d()
# TODO: Origin doesn't come out right.
o=rot@o.to_3d()
return o,tu,tv

def get_material_name(obj,material_index:int)->str:
""" Get material name using index. """
return obj.data.materials[material_index].name if len(obj.data.materials)>0 else ""

def normal_rotation(n1:Vector,n2:Vector)->Matrix:
""" Rotation matrix between normals n1 to n2. """
assert n1!=Vector((0,0,0)) and n2!=Vector((0,0,0))
# Angle between two.
cos_theta:float=n1.dot(n2)/(n1.length*n2.length)
try:
theta:float=math.acos(cos_theta)
except ValueError:
theta=0
# Rotation axis.
axis:Vector=n1.cross(n2)
axis.normalize()
# Calculate rotation matrix using Rodrigues' formula.
axis_skew=Matrix((
(0, -axis.z, axis.y),
(axis.z, 0, -axis.x),
(-axis.y, axis.x, 0)
))
i:Matrix=Matrix.Identity(3)
r:Matrix=i+axis_skew*math.sin(theta)+axis_skew@axis_skew*(1-math.cos(theta))
return r

def polygon_texture_transform(face:'bmesh.types.BMFace',mesh:'bmesh.types.BMesh')->tuple:
""" Compute the Origin, TextureU, TextureV for a given face. """
points:list[bmesh.types.BMLoop]=face.loops[0:3]
if len(points)<2 or len(mesh.loops.layers.uv)==0:
print("Invalid geometry or no UV map.")
return ((0,0,0),(1,0,0),(0,1,0))
uvmap=mesh.loops.layers.uv[0]
verts:list[Vector]=[x.vert.co for x in points] # type: ignore
uvs:list[Vector]=[x[uvmap].uv for x in points] # type: ignore
return export_uv(verts,uvs,face.normal)

def rotate_triangle_towards_normal(points:list[Vector],n:Vector)->list:
""" Return points after plane is rotated towards n. """
assert len(points)==3,"Not a triangle."
plane_normal:Vector=geometry.normal(points)
rotation:Matrix=normal_rotation(plane_normal,n)
return [rotation@p for p in points]

def transpose(matrix:Matrix)->Matrix:
""" mathutils.Matrix.transpose only works on square. """
return Matrix([[row[i] for row in matrix] for i in range(len(matrix[0]))])
Loading

0 comments on commit 7f77cfd

Please sign in to comment.