-
Notifications
You must be signed in to change notification settings - Fork 277
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
bugs in sdf_exporter.py #2678
Open
mahit2609
wants to merge
1
commit into
gazebosim:gz-sim9
Choose a base branch
from
mahit2609:gz-sim9
base: gz-sim9
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
bugs in sdf_exporter.py #2678
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,235 +1,217 @@ | ||
import xml.etree.ElementTree as ET | ||
from os import path | ||
from xml.dom import minidom | ||
|
||
import bpy | ||
from bpy.props import StringProperty | ||
from bpy.types import Operator | ||
import os.path | ||
from bpy_extras.io_utils import ImportHelper | ||
from bpy.props import StringProperty, BoolProperty | ||
from bpy.types import Operator | ||
|
||
# Tested Blender version: 2.82/3.2 | ||
import xml.etree.ElementTree as ET | ||
from xml.dom import minidom | ||
|
||
######################################################################################################################## | ||
### Exports model.dae of the scene with textures, its corresponding model.sdf file, and a default model.config file #### | ||
######################################################################################################################## | ||
def export_sdf(prefix_path): | ||
|
||
dae_filename = "model.dae" | ||
sdf_filename = "model.sdf" | ||
model_config_filename = "model.config" | ||
lightmap_filename = "LightmapBaked.png" | ||
model_name = "my_model" | ||
meshes_folder_prefix = "meshes/" | ||
|
||
dae_filename = 'model.dae' | ||
sdf_filename = 'model.sdf' | ||
model_config_filename = 'model.config' | ||
lightmap_filename = 'LightmapBaked.png' | ||
model_name = 'my_model' | ||
meshes_folder_prefix = 'meshes/' | ||
|
||
# Exports the dae file and its associated textures | ||
bpy.ops.wm.collada_export( | ||
filepath=path.join(prefix_path, meshes_folder_prefix, dae_filename), | ||
check_existing=False, | ||
filter_blender=False, | ||
filter_image=False, | ||
filter_movie=False, | ||
filter_python=False, | ||
filter_font=False, | ||
filter_sound=False, | ||
filter_text=False, | ||
filter_btx=False, | ||
filter_collada=True, | ||
filter_folder=True, | ||
filemode=8, | ||
) | ||
bpy.ops.wm.collada_export(filepath=prefix_path+meshes_folder_prefix+dae_filename, | ||
check_existing=False, | ||
filter_blender=False, | ||
filter_image=False, | ||
filter_movie=False, | ||
filter_python=False, | ||
filter_font=False, | ||
filter_sound=False, | ||
filter_text=False, | ||
filter_btx=False, | ||
filter_collada=True, | ||
filter_folder=True, | ||
filemode=8) | ||
|
||
# objects = bpy.context.selected_objects | ||
objects = bpy.context.selectable_objects | ||
mesh_objects = [o for o in objects if o.type == "MESH"] | ||
light_objects = [o for o in objects if o.type == "LIGHT"] | ||
|
||
############################################# | ||
#### export sdf xml based off the scene ##### | ||
############################################# | ||
sdf = ET.Element("sdf", attrib={"version": "1.8"}) | ||
mesh_objects = [o for o in objects if o.type == 'MESH'] | ||
light_objects = [o for o in objects if o.type == 'LIGHT'] | ||
|
||
# 1 model and 1 link | ||
model = ET.SubElement(sdf, "model", attrib={"name": "test"}) | ||
sdf = ET.Element('sdf', attrib={'version':'1.8'}) | ||
model = ET.SubElement(sdf, "model", attrib={"name":"test"}) | ||
static = ET.SubElement(model, "static") | ||
static.text = "true" | ||
link = ET.SubElement(model, "link", attrib={"name": "testlink"}) | ||
link = ET.SubElement(model, "link", attrib={"name":"testlink"}) | ||
|
||
def get_diffuse_map(material): | ||
"""Helper function to safely get diffuse map from material""" | ||
if not material or not material.use_nodes or not material.node_tree: | ||
return "" | ||
|
||
nodes = material.node_tree.nodes | ||
principled = next((n for n in nodes if n.type == 'BSDF_PRINCIPLED'), None) | ||
|
||
if not principled: | ||
return "" | ||
|
||
base_color = principled.inputs.get('Base Color') or principled.inputs[0] | ||
if not base_color.links: | ||
return "" | ||
|
||
link_node = base_color.links[0].from_node | ||
if not hasattr(link_node, 'image') or not link_node.image: | ||
return "" | ||
|
||
return link_node.image.name | ||
|
||
# for each geometry in geometry library add a <visual> tag | ||
for o in mesh_objects: | ||
visual = ET.SubElement(link, "visual", attrib={"name": o.name}) | ||
|
||
visual = ET.SubElement(link, "visual", attrib={"name":o.name}) | ||
geometry = ET.SubElement(visual, "geometry") | ||
mesh = ET.SubElement(geometry, "mesh") | ||
uri = ET.SubElement(mesh, "uri") | ||
uri.text = path.join(meshes_folder_prefix, dae_filename) | ||
uri.text = dae_filename | ||
submesh = ET.SubElement(mesh, "submesh") | ||
submesh_name = ET.SubElement(submesh, "name") | ||
submesh_name.text = o.name | ||
|
||
# grab diffuse/albedo map | ||
diffuse_map = "" | ||
if o.active_material is not None: | ||
nodes = o.active_material.node_tree.nodes | ||
principled = next(n for n in nodes if n.type == "BSDF_PRINCIPLED") | ||
if principled is not None: | ||
base_color = principled.inputs["Base Color"] # Or principled.inputs[0] | ||
value = base_color.default_value | ||
if len(base_color.links): | ||
link_node = base_color.links[0].from_node | ||
diffuse_map = link_node.image.name | ||
|
||
# setup diffuse/specular color | ||
|
||
# Material handling | ||
material = ET.SubElement(visual, "material") | ||
diffuse = ET.SubElement(material, "diffuse") | ||
diffuse.text = "1.0 1.0 1.0 1.0" | ||
specular = ET.SubElement(material, "specular") | ||
specular.text = "0.0 0.0 0.0 1.0" | ||
pbr = ET.SubElement(material, "pbr") | ||
metal = ET.SubElement(pbr, "metal") | ||
if diffuse_map != "": | ||
albedo_map = ET.SubElement(metal, "albedo_map") | ||
albedo_map.text = path.join(meshes_folder_prefix, diffuse_map) | ||
|
||
# for lightmapping, add the UV and turn off casting of shadows | ||
if path.isfile(lightmap_filename): | ||
light_map = ET.SubElement(metal, "light_map", attrib={"uv_set": "1"}) | ||
light_map.text = path.join(meshes_folder_prefix, lightmap_filename) | ||
|
||
|
||
# Get diffuse map if material exists | ||
if o.active_material: | ||
diffuse_map = get_diffuse_map(o.active_material) | ||
if diffuse_map: | ||
albedo_map = ET.SubElement(metal, "albedo_map") | ||
albedo_map.text = meshes_folder_prefix + diffuse_map | ||
|
||
# Lightmap handling | ||
if os.path.isfile(lightmap_filename): | ||
light_map = ET.SubElement(metal, "light_map", attrib={"uv_set":"1"}) | ||
light_map.text = meshes_folder_prefix + lightmap_filename | ||
cast_shadows = ET.SubElement(visual, "cast_shadows") | ||
cast_shadows.text = "0" | ||
|
||
def add_attenuation_tags(light_tag, blender_light): | ||
attenuation = ET.SubElement(light, "attenuation") | ||
range = ET.SubElement(attenuation, "range") | ||
range.text = str(blender_light.cutoff_distance) | ||
linear_attenuation = ET.SubElement(attenuation, "linear") | ||
linear_attenuation.text = str(blender_pointlight.linear_attenuation) | ||
quad_attenuation = ET.SubElement(attenuation, "quadratic") | ||
quad_attenuation.text = str(blender_pointlight.quadratic_coefficient) | ||
const_attenuation = ET.SubElement(attenuation, "constant") | ||
const_attenuation.text = str(blender_pointlight.constant_coefficient) | ||
|
||
# export lights | ||
"""Helper function to add attenuation tags based on light type""" | ||
attenuation = ET.SubElement(light_tag, "attenuation") | ||
|
||
# Range (cutoff distance) | ||
range_tag = ET.SubElement(attenuation, "range") | ||
range_tag.text = str(blender_light.cutoff_distance) | ||
|
||
# Linear attenuation factor | ||
linear = ET.SubElement(attenuation, "linear") | ||
linear.text = "1.0" # Default linear attenuation | ||
|
||
# Quadratic attenuation factor | ||
quadratic = ET.SubElement(attenuation, "quadratic") | ||
quadratic.text = "0.0" # Default quadratic attenuation | ||
|
||
# Constant attenuation factor | ||
constant = ET.SubElement(attenuation, "constant") | ||
constant.text = "1.0" # Default constant attenuation | ||
|
||
# Export lights | ||
for l in light_objects: | ||
blender_light = l.data | ||
|
||
if blender_light.type == "POINT": | ||
light = ET.SubElement( | ||
link, "light", attrib={"name": l.name, "type": "point"} | ||
) | ||
light = ET.SubElement(link, "light", attrib={"name":l.name, "type":"point"}) | ||
diffuse = ET.SubElement(light, "diffuse") | ||
diffuse.text = f"{blender_light.color.r} {blender_light.color.g} {blender_light.color.b} 1.0" | ||
blender_pointlight = bpy.types.PointLight(blender_light) | ||
|
||
add_attenuation_tags(light, blender_pointlight) | ||
|
||
if blender_light.type == "SPOT": | ||
light = ET.SubElement( | ||
link, "light", attrib={"name": l.name, "type": "spot"} | ||
) | ||
add_attenuation_tags(light, blender_light) | ||
|
||
elif blender_light.type == "SPOT": | ||
light = ET.SubElement(link, "light", attrib={"name":l.name, "type":"spot"}) | ||
diffuse = ET.SubElement(light, "diffuse") | ||
diffuse.text = f"{blender_light.color.r} {blender_light.color.g} {blender_light.color.b} 1.0" | ||
blender_spotlight = bpy.types.SpotLight(blender_light) | ||
|
||
add_attenuation_tags(light, blender_spotlight) | ||
# note: unsupported <spot> tags in blender | ||
|
||
if blender_light.type == "SUN": | ||
light = ET.SubElement( | ||
link, "light", attrib={"name": l.name, "type": "directional"} | ||
) | ||
add_attenuation_tags(light, blender_light) | ||
|
||
# Add spot light specific parameters | ||
spot = ET.SubElement(light, "spot") | ||
inner_angle = ET.SubElement(spot, "inner_angle") | ||
inner_angle.text = str(blender_light.spot_size * 0.5) # Convert to inner angle | ||
outer_angle = ET.SubElement(spot, "outer_angle") | ||
outer_angle.text = str(blender_light.spot_size) # Outer angle | ||
falloff = ET.SubElement(spot, "falloff") | ||
falloff.text = str(blender_light.spot_blend * 10) # Approximate falloff from blend | ||
|
||
elif blender_light.type == "SUN": | ||
light = ET.SubElement(link, "light", attrib={"name":l.name, "type":"directional"}) | ||
diffuse = ET.SubElement(light, "diffuse") | ||
diffuse.text = f"{blender_light.color.r} {blender_light.color.g} {blender_light.color.b} 1.0" | ||
blender_pointlight = bpy.types.SunLight(blender_light) | ||
|
||
if blender_light.type == "SUN" or blender_light.type == "SPOT": | ||
|
||
if blender_light.type in ["SUN", "SPOT"]: | ||
direction = ET.SubElement(light, "direction") | ||
direction.text = ( | ||
f"{l.matrix_world[0][2]} {l.matrix_world[1][2]} {l.matrix_world[2][2]}" | ||
) | ||
|
||
# unsupported: AREA lights | ||
direction.text = f"{l.matrix_world[0][2]} {l.matrix_world[1][2]} {l.matrix_world[2][2]}" | ||
|
||
# Common light properties | ||
cast_shadows = ET.SubElement(light, "cast_shadows") | ||
cast_shadows.text = "true" | ||
|
||
# todo : bpy.types.light script api lacks an intensity value, possible candidate is energy/power(Watts)? | ||
|
||
intensity = ET.SubElement(light, "intensity") | ||
intensity.text = "1.0" | ||
|
||
## sdf collision tags | ||
collision = ET.SubElement(link, "collision", attrib={"name": "collision"}) | ||
|
||
intensity.text = str(blender_light.energy) # Use the light's energy as intensity | ||
|
||
# Collision tags | ||
collision = ET.SubElement(link, "collision", attrib={"name":"collision"}) | ||
geometry = ET.SubElement(collision, "geometry") | ||
mesh = ET.SubElement(geometry, "mesh") | ||
uri = ET.SubElement(mesh, "uri") | ||
uri.text = path.join(meshes_folder_prefix, dae_filename) | ||
|
||
uri.text = dae_filename | ||
surface = ET.SubElement(collision, "surface") | ||
contact = ET.SubElement(surface, "contact") | ||
collide_bitmask = ET.SubElement(contact, "collide_bitmask") | ||
contact = ET.SubElement(collision, "contact") | ||
collide_bitmask = ET.SubElement(collision, "collide_bitmask") | ||
collide_bitmask.text = "0x01" | ||
|
||
## sdf write to file | ||
xml_string = ET.tostring(sdf, encoding="unicode") | ||
# Write SDF file | ||
xml_string = ET.tostring(sdf, encoding='unicode') | ||
reparsed = minidom.parseString(xml_string) | ||
with open(prefix_path+sdf_filename, "w") as sdf_file: | ||
sdf_file.write(reparsed.toprettyxml(indent=" ")) | ||
|
||
sdf_file = open(path.join(prefix_path, sdf_filename), "w") | ||
sdf_file.write(reparsed.toprettyxml(indent=" ")) | ||
sdf_file.close() | ||
|
||
############################## | ||
### generate model.config #### | ||
############################## | ||
model = ET.Element("model") | ||
name = ET.SubElement(model, "name") | ||
# Generate model.config | ||
model = ET.Element('model') | ||
name = ET.SubElement(model, 'name') | ||
name.text = model_name | ||
version = ET.SubElement(model, "version") | ||
version = ET.SubElement(model, 'version') | ||
version.text = "1.0" | ||
sdf_tag = ET.SubElement(model, "sdf", attrib={"sdf": "1.8"}) | ||
sdf_tag = ET.SubElement(model, "sdf", attrib={"version":"1.8"}) | ||
sdf_tag.text = sdf_filename | ||
|
||
author = ET.SubElement(model, "author") | ||
name = ET.SubElement(author, "name") | ||
author = ET.SubElement(model, 'author') | ||
name = ET.SubElement(author, 'name') | ||
name.text = "Generated by blender SDF tools" | ||
|
||
xml_string = ET.tostring(model, encoding="unicode") | ||
xml_string = ET.tostring(model, encoding='unicode') | ||
reparsed = minidom.parseString(xml_string) | ||
with open(prefix_path+model_config_filename, "w") as config_file: | ||
config_file.write(reparsed.toprettyxml(indent=" ")) | ||
|
||
config_file = open(path.join(prefix_path, model_config_filename), "w") | ||
config_file.write(reparsed.toprettyxml(indent=" ")) | ||
config_file.close() | ||
|
||
|
||
#### UI Handling #### | ||
class OT_TestOpenFilebrowser(Operator, ImportHelper): | ||
bl_idname = "test.open_filebrowser" | ||
bl_label = "Save" | ||
|
||
directory: StringProperty(name="Outdir Path") | ||
|
||
directory: bpy.props.StringProperty(name="Outdir Path") | ||
def execute(self, context): | ||
"""Do the export with the selected file.""" | ||
|
||
if not path.isdir(self.directory): | ||
print(f"{self.directory} is not a directory!") | ||
if not os.path.isdir(self.directory): | ||
print(self.directory + " is not a directory!") | ||
else: | ||
print(f"exporting to directory: {self.directory}") | ||
print("exporting to directory: " + self.directory) | ||
export_sdf(self.directory) | ||
return {"FINISHED"} | ||
|
||
return {'FINISHED'} | ||
|
||
def register(): | ||
bpy.utils.register_class(OT_TestOpenFilebrowser) | ||
def register(): | ||
bpy.utils.register_class(OT_TestOpenFilebrowser) | ||
|
||
|
||
def unregister(): | ||
def unregister(): | ||
bpy.utils.unregister_class(OT_TestOpenFilebrowser) | ||
|
||
|
||
|
||
if __name__ == "__main__": | ||
register() | ||
bpy.ops.test.open_filebrowser("INVOKE_DEFAULT") | ||
|
||
# alternatively comment the main code block and do a function call without going through all the ui | ||
# prefix_path = '/home/ddeng/blender_lightmap/final_office/office/' | ||
# export_sdf(prefix_path) | ||
register() | ||
bpy.ops.test.open_filebrowser('INVOKE_DEFAULT') |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know if this updated script still works with 2.82/3.2?