Skip to content

Commit f8df892

Browse files
committed
Fix: Updated sdf_exporter.py based on review feedback
Signed-off-by: mahit2609 <[email protected]>
1 parent 629adf6 commit f8df892

File tree

1 file changed

+135
-131
lines changed

1 file changed

+135
-131
lines changed
Lines changed: 135 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,217 +1,221 @@
1+
import xml.etree.ElementTree as ET
2+
from os import path
3+
from xml.dom import minidom
4+
15
import bpy
2-
import os.path
3-
from bpy_extras.io_utils import ImportHelper
4-
from bpy.props import StringProperty, BoolProperty
6+
from bpy.props import StringProperty
57
from bpy.types import Operator
8+
from bpy_extras.io_utils import ImportHelper
69

7-
import xml.etree.ElementTree as ET
8-
from xml.dom import minidom
10+
# Tested Blender version: 4.2/4.3
911

12+
########################################################################################################################
13+
### Exports model.dae of the scene with textures, its corresponding model.sdf file, and a default model.config file ####
14+
########################################################################################################################
1015
def export_sdf(prefix_path):
11-
dae_filename = 'model.dae'
12-
sdf_filename = 'model.sdf'
13-
model_config_filename = 'model.config'
14-
lightmap_filename = 'LightmapBaked.png'
15-
model_name = 'my_model'
16-
meshes_folder_prefix = 'meshes/'
17-
16+
17+
dae_filename = "model.dae"
18+
sdf_filename = "model.sdf"
19+
model_config_filename = "model.config"
20+
lightmap_filename = "LightmapBaked.png"
21+
model_name = "my_model"
22+
meshes_folder_prefix = "meshes/"
23+
1824
# Exports the dae file and its associated textures
19-
bpy.ops.wm.collada_export(filepath=prefix_path+meshes_folder_prefix+dae_filename,
20-
check_existing=False,
21-
filter_blender=False,
22-
filter_image=False,
23-
filter_movie=False,
24-
filter_python=False,
25-
filter_font=False,
26-
filter_sound=False,
27-
filter_text=False,
28-
filter_btx=False,
29-
filter_collada=True,
30-
filter_folder=True,
31-
filemode=8)
25+
bpy.ops.wm.collada_export(
26+
filepath=path.join(prefix_path, meshes_folder_prefix, dae_filename),
27+
check_existing=False,
28+
filter_blender=False,
29+
filter_image=False,
30+
filter_movie=False,
31+
filter_python=False,
32+
filter_font=False,
33+
filter_sound=False,
34+
filter_text=False,
35+
filter_btx=False,
36+
filter_collada=True,
37+
filter_folder=True,
38+
filemode=8,
39+
)
3240

41+
# objects = bpy.context.selected_objects
3342
objects = bpy.context.selectable_objects
34-
mesh_objects = [o for o in objects if o.type == 'MESH']
35-
light_objects = [o for o in objects if o.type == 'LIGHT']
43+
mesh_objects = [o for o in objects if o.type == "MESH"]
44+
light_objects = [o for o in objects if o.type == "LIGHT"]
45+
46+
#############################################
47+
#### export sdf xml based off the scene #####
48+
#############################################
49+
sdf = ET.Element("sdf", attrib={"version": "1.8"})
3650

37-
sdf = ET.Element('sdf', attrib={'version':'1.8'})
38-
model = ET.SubElement(sdf, "model", attrib={"name":"test"})
51+
# 1 model and 1 link
52+
model = ET.SubElement(sdf, "model", attrib={"name": "test"})
3953
static = ET.SubElement(model, "static")
4054
static.text = "true"
41-
link = ET.SubElement(model, "link", attrib={"name":"testlink"})
42-
43-
def get_diffuse_map(material):
44-
"""Helper function to safely get diffuse map from material"""
45-
if not material or not material.use_nodes or not material.node_tree:
46-
return ""
47-
48-
nodes = material.node_tree.nodes
49-
principled = next((n for n in nodes if n.type == 'BSDF_PRINCIPLED'), None)
50-
51-
if not principled:
52-
return ""
53-
54-
base_color = principled.inputs.get('Base Color') or principled.inputs[0]
55-
if not base_color.links:
56-
return ""
57-
58-
link_node = base_color.links[0].from_node
59-
if not hasattr(link_node, 'image') or not link_node.image:
60-
return ""
61-
62-
return link_node.image.name
63-
55+
link = ET.SubElement(model, "link", attrib={"name": "testlink"})
6456
# for each geometry in geometry library add a <visual> tag
6557
for o in mesh_objects:
66-
visual = ET.SubElement(link, "visual", attrib={"name":o.name})
67-
58+
visual = ET.SubElement(link, "visual", attrib={"name": o.name})
59+
6860
geometry = ET.SubElement(visual, "geometry")
6961
mesh = ET.SubElement(geometry, "mesh")
7062
uri = ET.SubElement(mesh, "uri")
71-
uri.text = dae_filename
63+
uri.text = path.join(meshes_folder_prefix, dae_filename)
7264
submesh = ET.SubElement(mesh, "submesh")
7365
submesh_name = ET.SubElement(submesh, "name")
7466
submesh_name.text = o.name
75-
76-
# Material handling
67+
68+
# grab diffuse/albedo map
69+
diffuse_map = ""
70+
if o.active_material is not None:
71+
nodes = o.active_material.node_tree.nodes
72+
principled = next(n for n in nodes if n.type == "BSDF_PRINCIPLED")
73+
if principled is not None:
74+
base_color = principled.inputs["Base Color"]
75+
if len(base_color.links):
76+
link_node = base_color.links[0].from_node
77+
diffuse_map = link_node.image.name
78+
79+
# setup diffuse/specular color
7780
material = ET.SubElement(visual, "material")
7881
diffuse = ET.SubElement(material, "diffuse")
7982
diffuse.text = "1.0 1.0 1.0 1.0"
8083
specular = ET.SubElement(material, "specular")
8184
specular.text = "0.0 0.0 0.0 1.0"
8285
pbr = ET.SubElement(material, "pbr")
8386
metal = ET.SubElement(pbr, "metal")
84-
85-
# Get diffuse map if material exists
86-
if o.active_material:
87-
diffuse_map = get_diffuse_map(o.active_material)
88-
if diffuse_map:
89-
albedo_map = ET.SubElement(metal, "albedo_map")
90-
albedo_map.text = meshes_folder_prefix + diffuse_map
91-
92-
# Lightmap handling
93-
if os.path.isfile(lightmap_filename):
94-
light_map = ET.SubElement(metal, "light_map", attrib={"uv_set":"1"})
95-
light_map.text = meshes_folder_prefix + lightmap_filename
87+
if diffuse_map != "":
88+
albedo_map = ET.SubElement(metal, "albedo_map")
89+
albedo_map.text = path.join(meshes_folder_prefix, diffuse_map)
90+
91+
# for lightmapping, add the UV and turn off casting of shadows
92+
if path.isfile(lightmap_filename):
93+
light_map = ET.SubElement(metal, "light_map", attrib={"uv_set": "1"})
94+
light_map.text = path.join(meshes_folder_prefix, lightmap_filename)
95+
9696
cast_shadows = ET.SubElement(visual, "cast_shadows")
9797
cast_shadows.text = "0"
9898

9999
def add_attenuation_tags(light_tag, blender_light):
100-
"""Helper function to add attenuation tags based on light type"""
101100
attenuation = ET.SubElement(light_tag, "attenuation")
102-
103-
# Range (cutoff distance)
104-
range_tag = ET.SubElement(attenuation, "range")
105-
range_tag.text = str(blender_light.cutoff_distance)
106-
107-
# Linear attenuation factor
101+
range = ET.SubElement(attenuation, "range")
102+
range.text = str(blender_light.cutoff_distance)
108103
linear = ET.SubElement(attenuation, "linear")
109-
linear.text = "1.0" # Default linear attenuation
110-
111-
# Quadratic attenuation factor
104+
linear.text = "1.0"
112105
quadratic = ET.SubElement(attenuation, "quadratic")
113-
quadratic.text = "0.0" # Default quadratic attenuation
114-
115-
# Constant attenuation factor
106+
quadratic.text = "0.0"
116107
constant = ET.SubElement(attenuation, "constant")
117-
constant.text = "1.0" # Default constant attenuation
108+
constant.text = "1.0"
109+
118110

119-
# Export lights
111+
# export lights
120112
for l in light_objects:
121113
blender_light = l.data
122-
114+
123115
if blender_light.type == "POINT":
124-
light = ET.SubElement(link, "light", attrib={"name":l.name, "type":"point"})
116+
light = ET.SubElement(link, "light", attrib={"name": l.name, "type": "point"})
125117
diffuse = ET.SubElement(light, "diffuse")
126118
diffuse.text = f"{blender_light.color.r} {blender_light.color.g} {blender_light.color.b} 1.0"
127119
add_attenuation_tags(light, blender_light)
128-
129-
elif blender_light.type == "SPOT":
130-
light = ET.SubElement(link, "light", attrib={"name":l.name, "type":"spot"})
120+
121+
if blender_light.type == "SPOT":
122+
light = ET.SubElement(link, "light", attrib={"name": l.name, "type": "spot"})
131123
diffuse = ET.SubElement(light, "diffuse")
132124
diffuse.text = f"{blender_light.color.r} {blender_light.color.g} {blender_light.color.b} 1.0"
133125
add_attenuation_tags(light, blender_light)
134126

135-
# Add spot light specific parameters
127+
# note: unsupported <spot> tags in blender
136128
spot = ET.SubElement(light, "spot")
137129
inner_angle = ET.SubElement(spot, "inner_angle")
138-
inner_angle.text = str(blender_light.spot_size * 0.5) # Convert to inner angle
130+
inner_angle.text = str(blender_light.spot_size * 0.5)
139131
outer_angle = ET.SubElement(spot, "outer_angle")
140-
outer_angle.text = str(blender_light.spot_size) # Outer angle
132+
outer_angle.text = str(blender_light.spot_size)
141133
falloff = ET.SubElement(spot, "falloff")
142-
falloff.text = str(blender_light.spot_blend * 10) # Approximate falloff from blend
143-
144-
elif blender_light.type == "SUN":
145-
light = ET.SubElement(link, "light", attrib={"name":l.name, "type":"directional"})
134+
falloff.text = str(blender_light.spot_blend * 10)
135+
136+
if blender_light.type == "SUN":
137+
light = ET.SubElement(link, "light", attrib={"name": l.name, "type": "directional"})
146138
diffuse = ET.SubElement(light, "diffuse")
147139
diffuse.text = f"{blender_light.color.r} {blender_light.color.g} {blender_light.color.b} 1.0"
148-
140+
149141
if blender_light.type in ["SUN", "SPOT"]:
150142
direction = ET.SubElement(light, "direction")
151143
direction.text = f"{l.matrix_world[0][2]} {l.matrix_world[1][2]} {l.matrix_world[2][2]}"
152144

153-
# Common light properties
145+
# unsupported: AREA lights
146+
154147
cast_shadows = ET.SubElement(light, "cast_shadows")
155148
cast_shadows.text = "true"
156-
149+
150+
# todo : bpy.types.light script api lacks an intensity value, possible candidate is energy/power(Watts)?
157151
intensity = ET.SubElement(light, "intensity")
158-
intensity.text = str(blender_light.energy) # Use the light's energy as intensity
159-
160-
# Collision tags
161-
collision = ET.SubElement(link, "collision", attrib={"name":"collision"})
152+
intensity.text = str(blender_light.energy)
153+
154+
## sdf collision tags
155+
collision = ET.SubElement(link, "collision", attrib={"name": "collision"})
156+
162157
geometry = ET.SubElement(collision, "geometry")
163158
mesh = ET.SubElement(geometry, "mesh")
164159
uri = ET.SubElement(mesh, "uri")
165-
uri.text = dae_filename
160+
uri.text = path.join(meshes_folder_prefix, dae_filename)
161+
166162
surface = ET.SubElement(collision, "surface")
167-
contact = ET.SubElement(collision, "contact")
168-
collide_bitmask = ET.SubElement(collision, "collide_bitmask")
163+
contact = ET.SubElement(surface, "contact")
164+
collide_bitmask = ET.SubElement(contact, "collide_bitmask")
169165
collide_bitmask.text = "0x01"
170166

171-
# Write SDF file
172-
xml_string = ET.tostring(sdf, encoding='unicode')
167+
## sdf write to file
168+
xml_string = ET.tostring(sdf, encoding="unicode")
173169
reparsed = minidom.parseString(xml_string)
174-
with open(prefix_path+sdf_filename, "w") as sdf_file:
170+
with open(path.join(prefix_path, sdf_filename), "w") as sdf_file:
175171
sdf_file.write(reparsed.toprettyxml(indent=" "))
176172

177-
# Generate model.config
178-
model = ET.Element('model')
179-
name = ET.SubElement(model, 'name')
173+
##############################
174+
### generate model.config ####
175+
##############################
176+
model = ET.Element("model")
177+
name = ET.SubElement(model, "name")
180178
name.text = model_name
181-
version = ET.SubElement(model, 'version')
179+
version = ET.SubElement(model, "version")
182180
version.text = "1.0"
183-
sdf_tag = ET.SubElement(model, "sdf", attrib={"version":"1.8"})
181+
sdf_tag = ET.SubElement(model, "sdf", attrib={"version": "1.8"})
184182
sdf_tag.text = sdf_filename
185183

186-
author = ET.SubElement(model, 'author')
187-
name = ET.SubElement(author, 'name')
184+
author = ET.SubElement(model, "author")
185+
name = ET.SubElement(author, "name")
188186
name.text = "Generated by blender SDF tools"
189187

190-
xml_string = ET.tostring(model, encoding='unicode')
188+
xml_string = ET.tostring(model, encoding="unicode")
191189
reparsed = minidom.parseString(xml_string)
192-
with open(prefix_path+model_config_filename, "w") as config_file:
190+
with open(path.join(prefix_path, model_config_filename), "w") as config_file:
193191
config_file.write(reparsed.toprettyxml(indent=" "))
194192

193+
#### UI Handling ####
195194
class OT_TestOpenFilebrowser(Operator, ImportHelper):
196195
bl_idname = "test.open_filebrowser"
197196
bl_label = "Save"
198-
199-
directory: bpy.props.StringProperty(name="Outdir Path")
200-
197+
198+
directory: StringProperty(name="Outdir Path")
199+
201200
def execute(self, context):
202-
if not os.path.isdir(self.directory):
203-
print(self.directory + " is not a directory!")
201+
"""Do the export with the selected file."""
202+
if not path.isdir(self.directory):
203+
print(f"{self.directory} is not a directory!")
204204
else:
205-
print("exporting to directory: " + self.directory)
205+
print(f"exporting to directory: {self.directory}")
206206
export_sdf(self.directory)
207-
return {'FINISHED'}
207+
return {"FINISHED"}
208208

209-
def register():
210-
bpy.utils.register_class(OT_TestOpenFilebrowser)
209+
def register():
210+
bpy.utils.register_class(OT_TestOpenFilebrowser)
211211

212-
def unregister():
212+
def unregister():
213213
bpy.utils.unregister_class(OT_TestOpenFilebrowser)
214-
214+
215215
if __name__ == "__main__":
216-
register()
217-
bpy.ops.test.open_filebrowser('INVOKE_DEFAULT')
216+
register()
217+
bpy.ops.test.open_filebrowser("INVOKE_DEFAULT")
218+
219+
# alternatively comment the main code block and do a function call without going through all the ui
220+
# prefix_path = '/home/ddeng/blender_lightmap/final_office/office/'
221+
# export_sdf(prefix_path)

0 commit comments

Comments
 (0)