Skip to content

Commit fe6ddd5

Browse files
authored
Texture import guide
2 parents 7f77cfd + a3a9b54 commit fe6ddd5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+13105
-103
lines changed

blender_t3d/__init__.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# type: ignore ; tell Pylance to ignore props.
2-
bl_info={
2+
bl_info:dict[str,any]={
33
"name": "Import and export old Unreal .T3D format",
44
"author": "Crapola",
5-
"version": (1,1,0),
5+
"version": (1,1,1),
66
"blender": (3,3,0),
77
"location": "File > Import-Export ; Object",
88
"description": "Import and export UnrealED .T3D files.",
@@ -117,13 +117,16 @@ def execute(self,context):
117117
if not self.filename.split(".")[0]:
118118
self.report({'ERROR'},INVALID_FILENAME)
119119
return {'CANCELLED'}
120-
importer.import_t3d_file(
120+
121+
results:dict[str,list[str]]=importer.import_t3d_file(
121122
context,
122123
self.filepath,
123124
#self.filename,
124125
self.snap_vertices,
125126
self.snap_distance,
126127
self.flip)
128+
for w in results['WARNING']:
129+
self.report({'WARNING'},w)
127130
return {'FINISHED'}
128131

129132
def invoke(self, context, event):
@@ -144,18 +147,18 @@ def invoke(self, context, event):
144147
lambda x,_:x.layout.operator(BT3D_MT_file_import.bl_idname),
145148
)
146149

147-
def register():
148-
print("Registering.")
150+
def register()->None:
151+
#print("Registering.")
149152
register_classes()
150153
# Add to menu.
151154
bpy.types.VIEW3D_MT_object.append(menus[0])
152155
bpy.types.TOPBAR_MT_file_export.append(menus[1])
153156
bpy.types.TOPBAR_MT_file_import.append(menus[2])
154157

155-
def unregister():
156-
print("Unregistering.")
158+
def unregister()->None:
159+
#print("Unregistering.")
157160
# Remove from menu.
158161
bpy.types.VIEW3D_MT_object.remove(menus[0])
159162
bpy.types.TOPBAR_MT_file_export.remove(menus[1])
160163
bpy.types.TOPBAR_MT_file_import.remove(menus[2])
161-
unregister_classes()
164+
unregister_classes()

blender_t3d/blender_manifest.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
schema_version = "1.0.0"
2+
3+
id = "blender_t3d"
4+
version = "1.1.1"
5+
name = "Blender T3D"
6+
tagline = "Import and export old Unreal .T3D format"
7+
maintainer = "Crapola <https://github.com/crapola>"
8+
type = "add-on"
9+
website = "https://github.com/crapola/blender_t3d"
10+
# # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
11+
tags = ["Import-Export"]
12+
blender_version_min = "4.2.0"
13+
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
14+
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
15+
license = [
16+
"SPDX:GPL-3.0-or-later",
17+
]
18+
[permissions]
19+
files = "Import/export T3D from/to disk"
20+
clipboard = "Export brush to clipboard"

blender_t3d/exporter.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ def brush_from_object(o:'bpy.types.Object',scale_multiplier:float=1.0)->Brush|st
2424
""" Turn Blender Object into t3d.Brush. """
2525

2626
if o.type!="MESH":
27-
print(f"{o} is not a mesh.")
27+
_print(f"{o} is not a mesh.")
2828
return ""
2929

30-
print(f"Exporting {o.name}...")
30+
_print(f"Exporting {o.name}...")
3131

3232
bm:bmesh.types.BMesh=bmesh.new()
3333
bm.from_mesh(o.data)
@@ -59,7 +59,6 @@ def brush_from_object(o:'bpy.types.Object',scale_multiplier:float=1.0)->Brush|st
5959
brush.mainscale=o.scale
6060

6161
# Custom properties.
62-
#print(o.keys())
6362
brush.csg=o.get("csg",brush.csg)
6463
brush.group=o.get("group",brush.group)
6564
brush.polyflags=o.get("polyflags",brush.polyflags)
@@ -83,7 +82,7 @@ def export_uv(verts:list[Vector],uvs:list[Vector],normal:Vector)->tuple:
8382
uvs=[Vector((uv.x,1-uv.y))*TEXTURE_SIZE for uv in uvs]
8483
verts=rotate_triangle_towards_normal(verts,Vector((0,0,1)))
8584
_print("Rotated verts to XY plane:",verts)
86-
height=verts[0].z
85+
#height=verts[0].z
8786
verts=[v.xy for v in verts]
8887
_print("Flat Verts:",verts)
8988
m_uvs=Matrix((*[v.to_3d()+Vector((0,0,1)) for v in uvs],))
@@ -136,7 +135,7 @@ def polygon_texture_transform(face:'bmesh.types.BMFace',mesh:'bmesh.types.BMesh'
136135
""" Compute the Origin, TextureU, TextureV for a given face. """
137136
points:list[bmesh.types.BMLoop]=face.loops[0:3]
138137
if len(points)<2 or len(mesh.loops.layers.uv)==0:
139-
print("Invalid geometry or no UV map.")
138+
_print("Invalid geometry or no UV map.")
140139
return ((0,0,0),(1,0,0),(0,1,0))
141140
uvmap=mesh.loops.layers.uv[0]
142141
verts:list[Vector]=[x.vert.co for x in points] # type: ignore

blender_t3d/importer.py

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
"""
44
import math
55
import time
6+
import typing
67
from pathlib import Path
78

89
import bmesh
10+
from bmesh.types import BMLayerItem
911
import bpy
12+
from bpy.types import Material, Mesh
1013
from mathutils import Euler, Vector
1114

1215
try:
@@ -26,20 +29,33 @@ def convert_uv(vertex:Vector,
2629
v:Vector=vertex-origin
2730
return Vector((v.dot(texture_u),v.dot(texture_v)))+pan
2831

32+
def find_material(name:str)->Material|None:
33+
"""
34+
Case insensitive material search in Blender file.
35+
It returns the first match in cases of case collisions.
36+
"""
37+
for m in bpy.data.materials:
38+
if name.lower()==m.name.lower():
39+
return m
40+
return None
41+
2942
def material_index_by_name(obj,matname:str)->int:
3043
"""
3144
Get material index using material name in object.
3245
If material is not found on object, return 0.
3346
"""
34-
mat_dict = {mat.name: i for i, mat in enumerate(obj.data.materials)}
47+
mat_dict:dict[str,int]={mat.name: i for i, mat in enumerate(obj.data.materials)}
3548
try:
36-
ret=mat_dict[matname]
49+
ret:int=mat_dict[matname]
3750
return ret
3851
except KeyError:
3952
return 0
4053

41-
def create_object(collection:bpy.types.Collection,b:t3d.Brush)->bpy.types.Object:
54+
def create_object(collection:bpy.types.Collection,b:t3d.Brush)->tuple[bpy.types.Object,set[str]]:
4255
""" Create blender object from t3d.Brush. """
56+
# Keep track of missing materials for this object.
57+
missing_materials:set[str]=set()
58+
# Create mesh.
4359
m:bpy.types.Mesh=bpy.data.meshes.new(b.actor_name)
4460
m.from_pydata(*b.get_pydata())
4561
# Create object.
@@ -52,9 +68,9 @@ def create_object(collection:bpy.types.Collection,b:t3d.Brush)->bpy.types.Object
5268
mainscale=Vector(b.mainscale or (1,1,1))
5369
pivot=Vector(b.prepivot or (0,0,0))
5470
postscale=Vector(b.postscale or (1,1,1))
55-
rotation=Vector(b.rotation or (0,0,0))*math.tau/65536
71+
rotation:Vector|Euler=Vector(b.rotation or (0,0,0))*math.tau/65536
5672
rotation.xy=-rotation.xy
57-
rotation=Euler(rotation)
73+
rotation=Euler(rotation.to_tuple())
5874

5975
pivot.rotate(rotation)
6076
pivot*=postscale*mainscale
@@ -68,28 +84,35 @@ def create_object(collection:bpy.types.Collection,b:t3d.Brush)->bpy.types.Object
6884
bm:bmesh.types.BMesh=bmesh.new()
6985
bm.from_mesh(m)
7086
# Create UV layer.
71-
#uv_map=o.data.uv_layers.new(name='uvmap')
72-
uv_layer=bm.loops.layers.uv.verify()
87+
uv_layer:bmesh.types.BMLayerItem=bm.loops.layers.uv.verify()
7388
# Polygon attributes.
74-
texture_names=[p.texture for p in b.polygons]
75-
flags=[p.flags for p in b.polygons]
76-
layer_texture=bm.faces.layers.string.get("texture") or bm.faces.layers.string.new("texture")
77-
layer_flags=bm.faces.layers.int.get("flags") or bm.faces.layers.int.new("flags")
89+
texture_names:list[str]=[p.texture for p in b.polygons]
90+
flags:list[int]=[p.flags for p in b.polygons]
91+
layer_texture:BMLayerItem[bytes]=bm.faces.layers.string.get("texture") or bm.faces.layers.string.new("texture")
92+
layer_flags:BMLayerItem[int]=bm.faces.layers.int.get("flags") or bm.faces.layers.int.new("flags")
93+
i:int
94+
face:bmesh.types.BMFace
7895
for i,face in enumerate(bm.faces):
7996
if texture_names[i]:
8097
face[layer_texture]=bytes(str(texture_names[i]),'utf-8')
81-
# Note: material names are case sensitive in Blender.
82-
scene_mat=bpy.data.materials.get(texture_names[i])
83-
# Add material to object if it's not there yet.
84-
if scene_mat and not texture_names[i] in o.data.materials:
85-
o.data.materials.append(scene_mat)
86-
# Assign the face.
87-
face.material_index=material_index_by_name(o,texture_names[i])
98+
# Note: material names are case sensitive in Blender but
99+
# not in UnrealEd.
100+
scene_mat:Material|None=find_material(texture_names[i])
101+
if scene_mat:
102+
object_mesh_data:Mesh=typing.cast(Mesh,o.data)
103+
# Add material to object if it's not there yet.
104+
if not scene_mat.name in object_mesh_data.materials:
105+
object_mesh_data.materials.append(scene_mat)
106+
# Assign the face.
107+
face.material_index=material_index_by_name(o,scene_mat.name)
108+
else:
109+
# Missing material.
110+
missing_materials.add(texture_names[i])
88111
face[layer_flags]=flags[i]
89112
# UV coordinates.
90-
poly=b.polygons[i]
113+
poly:t3d.Polygon=b.polygons[i]
91114
for loop in face.loops:
92-
vert=loop.vert.co
115+
vert:Vector=loop.vert.co
93116
tu=Vector(poly.u)
94117
tv=Vector(poly.v)
95118
origin=Vector(poly.origin)
@@ -104,7 +127,6 @@ def create_object(collection:bpy.types.Collection,b:t3d.Brush)->bpy.types.Object
104127
collection.objects.link(o)
105128
# PostScale requires applying previous transforms.
106129
if b.postscale:
107-
#print("Postscale ",b.postscale)
108130
o.select_set(True)
109131
bpy.context.view_layer.objects.active=o
110132
bpy.ops.object.transform_apply(scale=True,rotation=True,location=False)
@@ -115,36 +137,44 @@ def create_object(collection:bpy.types.Collection,b:t3d.Brush)->bpy.types.Object
115137
o["group"]=b.group
116138
o["polyflags"]=b.polyflags
117139

118-
return o
140+
return o,missing_materials
119141

120142
def import_t3d_file(
121143
context:bpy.types.Context,
122144
filepath:str,
123145
snap_vertices:bool,
124146
snap_distance:float,
125-
flip:bool,
126-
)->None:
147+
flip:bool
148+
)->dict[str,list[str]]:
127149
""" Import T3D file into scene. """
128-
150+
# Missing materials that will be reported.
151+
missing_materials:set[str]=set()
129152
# Parse T3D file.
130153
brushes:list[t3d.Brush]=t3d_parser.t3d_open(filepath)
131154
time_start:float=time.time()
132155
# Create a collection bearing the T3D file's name.
133156
coll:bpy.types.Collection=bpy.data.collections.new(Path(filepath).name)
157+
# Add it to the scene.
134158
context.scene.collection.children.link(coll)
135159
# Turn every t3d.Brush into a Blender object.
136160
for b in brushes:
137-
#print(f"Importing {b.actor_name}...")
161+
obj:bpy.types.Object
138162
if b.group=='cube':
139163
# Ignore red brush.
140-
print(f"blender_t3d import: {b.actor_name} is the red brush, so it won't be imported.")
164+
print(f"blender_t3d: {b.actor_name} is the red brush, so it won't be imported.")
141165
continue
142166
# Snap to grid.
143167
if snap_vertices:
144168
b.snap(snap_distance)
145-
obj=create_object(coll,b)
169+
obj_missing_mats:set[str]
170+
obj,obj_missing_mats=create_object(coll,b)
171+
missing_materials.update(obj_missing_mats)
146172
# Flip.
147173
if b.csg.lower()=="csg_subtract" and flip:
148174
obj.data.flip_normals()
149175
# Output time to console.
150-
print(f"Created {len(brushes)} meshes in {time.time()-time_start} seconds.")
176+
print(f"blender_t3d: Created {len(brushes)} meshes in {time.time()-time_start} seconds.")
177+
results:dict={"WARNING":[]}
178+
if missing_materials:
179+
results["WARNING"]=[f"{len(missing_materials)} materials missing: {', '.join(sorted(missing_materials))}"]
180+
return results

blender_t3d/t3d_parser.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
T3D parser.
33
"""
44
import ast
5-
#import pprint
65
import re
76
from enum import IntEnum, auto
87

@@ -27,13 +26,12 @@ def filter_brushes(text:str)->str:
2726
Filter T3D text to remove anything that's not a Brush actor.
2827
Return the modified text.
2928
"""
30-
pattern:str=r"""(Begin Actor Class=Brush .*?End Actor)"""
29+
pattern:str=r"""(Begin Actor Class=(?:Engine.)?Brush .*?End Actor)"""
3130
rx:re.Pattern=re.compile(pattern,re.S|re.I)
3231
matches:list=rx.findall(text)
3332
ret:str="\n".join(matches)
3433
return ret
3534

36-
3735
class Level(IntEnum):
3836
""" Current nesting level. """
3937
ROOT=0
@@ -67,7 +65,6 @@ def dict_from_t3d_property(line:str)->dict:
6765
d=ast.literal_eval(x)
6866
except (ValueError,SyntaxError):
6967
# Errors might happen on lines we don't care about.
70-
#print(line," ",x)
7168
pass
7269
return d
7370

@@ -108,7 +105,7 @@ def parse_polygon_property(line:str)->dict[str,tuple]:
108105
try:
109106
keyword:str
110107
data:str
111-
line:str=line.replace("\t"," ") # TODO: all file
108+
line=line.replace("\t"," ") # TODO: all file
112109
keyword,data=line.strip().split(" ",1)
113110
value:tuple=()
114111
if keyword=="pan":
@@ -192,7 +189,6 @@ def t3d_open(path:str)->list[t3d.Brush]:
192189
# Convert dictionaries to t3d.Brush.
193190
tbs:list[t3d.Brush]=[]
194191
for b in brushes:
195-
#pprint.pprint(b)
196192
# Convert values to tuples.
197193
if b.get("mainscale") and b.get("mainscale",{}).get("scale"):
198194
b["mainscale"]["scale"]=coords_from_xyz_dict(b["mainscale"]["scale"],1.0)
@@ -206,24 +202,24 @@ def t3d_open(path:str)->list[t3d.Brush]:
206202
b["rotation"]=rotation_from_dict(b["rotation"])
207203
tb:t3d.Brush=t3d.Brush.from_dictionary(b)
208204
tbs.append(tb)
209-
print(f"Loaded {len(tbs)} brushes from {path} in {time.time()-time_start} seconds.")
205+
print(f"blender_t3d: Loaded {len(tbs)} brushes from {path} in {time.time()-time_start} seconds.")
210206
return tbs
211207

212208
def test()->None:
213209
""" Test. """
214210
samples_list:tuple[str,...]=(
215-
"dev/samples/swat/fairfax-swat4.t3d",
216-
"dev/samples/swat/map-ue2.t3d",
217-
"dev/samples/swat/streets-raveshield.t3d",
218-
"dev/samples/ut99/AS-Frigate.t3d",
219-
"dev/samples/ut99/CTF-Coret.t3d",
220-
"dev/samples/ut99/DM-Liandri.t3d",
221-
"dev/samples/ut99/DOM-Cinder.t3d",
222-
"dev/samples/ut2004/AS-FallenCity.t3d",
223-
"dev/samples/ut2004/BR-Anubis.t3d",
224-
"dev/samples/ut2004/DM-Deck17.t3d",
225-
"dev/samples/xiii/DM_Amos.t3d",
226-
"dev/samples/xiii/xiii_cubes.t3d"
211+
"development/samples/swat/fairfax-swat4.t3d",
212+
"development/samples/swat/map-ue2.t3d",
213+
"development/samples/swat/streets-raveshield.t3d",
214+
"development/samples/ut99/AS-Frigate.t3d",
215+
"development/samples/ut99/CTF-Coret.t3d",
216+
"development/samples/ut99/DM-Liandri.t3d",
217+
"development/samples/ut99/DOM-Cinder.t3d",
218+
"development/samples/ut2004/AS-FallenCity.t3d",
219+
"development/samples/ut2004/BR-Anubis.t3d",
220+
"development/samples/ut2004/DM-Deck17.t3d",
221+
"development/samples/xiii/DM_Amos.t3d",
222+
"development/samples/xiii/xiii_cubes.t3d"
227223
)
228224
for s in samples_list:
229225
b:list[t3d.Brush]=t3d_open(s)

dev/install.ps1

Lines changed: 0 additions & 10 deletions
This file was deleted.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)