diff options
author | Campbell Barton <ideasman42@gmail.com> | 2011-01-14 20:33:39 +0300 |
---|---|---|
committer | Campbell Barton <ideasman42@gmail.com> | 2011-01-14 20:33:39 +0300 |
commit | f47802fbfebf98be32534ae8385f0e815694eb52 (patch) | |
tree | 8c8cd20015c2e9fe18a1a1d3f7347d430eabf2e5 /io_scene_x3d | |
parent | c7c6d6d879875a3734a73eafee652488d2d10534 (diff) |
scripts from bf-blender which are now maintained in bf-extensions.
Diffstat (limited to 'io_scene_x3d')
-rw-r--r-- | io_scene_x3d/__init__.py | 96 | ||||
-rw-r--r-- | io_scene_x3d/export_x3d.py | 847 | ||||
-rw-r--r-- | io_scene_x3d/import_x3d.py | 2658 |
3 files changed, 3601 insertions, 0 deletions
diff --git a/io_scene_x3d/__init__.py b/io_scene_x3d/__init__.py new file mode 100644 index 00000000..67d35ce9 --- /dev/null +++ b/io_scene_x3d/__init__.py @@ -0,0 +1,96 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +bl_info = { + "name": "Web3D X3D/VRML format", + "author": "Campbell Barton, Bart", + "location": "File > Import-Export", + "description": "Import-Export X3D, Import VRML", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\ + "Scripts/Import-Export/Web3D", + "tracker_url": "", + "support": 'OFFICIAL', + "category": "Import-Export"} + +# To support reload properly, try to access a package var, if it's there, reload everything +if "bpy" in locals(): + import imp + if "export_x3d" in locals(): + imp.reload(export_x3d) + + +import bpy +from bpy.props import * +from io_utils import ImportHelper, ExportHelper + + +class ImportX3D(bpy.types.Operator, ImportHelper): + '''Load a BVH motion capture file''' + bl_idname = "import_scene.x3d" + bl_label = "Import X3D/VRML" + + filename_ext = ".x3d" + filter_glob = StringProperty(default="*.x3d;*.wrl", options={'HIDDEN'}) + + def execute(self, context): + from . import import_x3d + return import_x3d.load(self, context, **self.as_keywords(ignore=("filter_glob",))) + + +class ExportX3D(bpy.types.Operator, ExportHelper): + '''Export selection to Extensible 3D file (.x3d)''' + bl_idname = "export_scene.x3d" + bl_label = 'Export X3D' + + filename_ext = ".x3d" + filter_glob = StringProperty(default="*.x3d", options={'HIDDEN'}) + + use_apply_modifiers = BoolProperty(name="Apply Modifiers", description="Use transformed mesh data from each object", default=True) + use_triangulate = BoolProperty(name="Triangulate", description="Triangulate quads.", default=False) + use_compress = BoolProperty(name="Compress", description="GZip the resulting file, requires a full python install", default=False) + + def execute(self, context): + from . import export_x3d + return export_x3d.save(self, context, **self.as_keywords(ignore=("check_existing", "filter_glob"))) + + +def menu_func_import(self, context): + self.layout.operator(ImportX3D.bl_idname, text="X3D Extensible 3D (.x3d/.wrl)") + + +def menu_func_export(self, context): + self.layout.operator(ExportX3D.bl_idname, text="X3D Extensible 3D (.x3d)") + + +def register(): + bpy.types.INFO_MT_file_import.append(menu_func_import) + bpy.types.INFO_MT_file_export.append(menu_func_export) + + +def unregister(): + bpy.types.INFO_MT_file_import.remove(menu_func_import) + bpy.types.INFO_MT_file_export.remove(menu_func_export) + +# NOTES +# - blender version is hardcoded + +if __name__ == "__main__": + register() diff --git a/io_scene_x3d/export_x3d.py b/io_scene_x3d/export_x3d.py new file mode 100644 index 00000000..c420b0cd --- /dev/null +++ b/io_scene_x3d/export_x3d.py @@ -0,0 +1,847 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# Contributors: bart:neeneenee*de, http://www.neeneenee.de/vrml, Campbell Barton + +""" +This script exports to X3D format. + +Usage: +Run this script from "File->Export" menu. A pop-up will ask whether you +want to export only selected or all relevant objects. + +Known issues: + Doesn't handle multiple materials (don't use material indices);<br> + Doesn't handle multiple UV textures on a single mesh (create a mesh for each texture);<br> + Can't get the texture array associated with material * not the UV ones; +""" + +import math +import os + +import bpy +import mathutils + +from io_utils import create_derived_objects, free_derived_objects + + +def round_color(col, cp): + return tuple([round(max(min(c, 1.0), 0.0), cp) for c in col]) + + +def matrix_direction(mtx): + return (mathutils.Vector((0.0, 0.0, -1.0)) * mtx.rotation_part()).normalize()[:] + + +########################################################## +# Functions for writing output file +########################################################## + + +class x3d_class: + + def __init__(self, filepath): + #--- public you can change these --- + self.proto = 1 + self.billnode = 0 + self.halonode = 0 + self.collnode = 0 + self.verbose = 2 # level of verbosity in console 0-none, 1-some, 2-most + self.cp = 3 # decimals for material color values 0.000 - 1.000 + self.vp = 3 # decimals for vertex coordinate values 0.000 - n.000 + self.tp = 3 # decimals for texture coordinate values 0.000 - 1.000 + self.it = 3 + + self.global_matrix = mathutils.Matrix.Rotation(-(math.pi / 2.0), 4, 'X') + + #--- class private don't touch --- + self.indentLevel = 0 # keeps track of current indenting + self.filepath = filepath + self.file = None + if filepath.lower().endswith('.x3dz'): + try: + import gzip + self.file = gzip.open(filepath, "w") + except: + print("failed to import compression modules, exporting uncompressed") + self.filepath = filepath[:-1] # remove trailing z + + if self.file is None: + self.file = open(self.filepath, "w", encoding='utf8') + + self.bNav = 0 + self.nodeID = 0 + self.namesReserved = ("Anchor", "Appearance", "Arc2D", "ArcClose2D", "AudioClip", "Background", "Billboard", + "BooleanFilter", "BooleanSequencer", "BooleanToggle", "BooleanTrigger", "Box", "Circle2D", + "Collision", "Color", "ColorInterpolator", "ColorRGBA", "component", "Cone", "connect", + "Contour2D", "ContourPolyline2D", "Coordinate", "CoordinateDouble", "CoordinateInterpolator", + "CoordinateInterpolator2D", "Cylinder", "CylinderSensor", "DirectionalLight", "Disk2D", + "ElevationGrid", "EspduTransform", "EXPORT", "ExternProtoDeclare", "Extrusion", "field", + "fieldValue", "FillProperties", "Fog", "FontStyle", "GeoCoordinate", "GeoElevationGrid", + "GeoLocationLocation", "GeoLOD", "GeoMetadata", "GeoOrigin", "GeoPositionInterpolator", + "GeoTouchSensor", "GeoViewpoint", "Group", "HAnimDisplacer", "HAnimHumanoid", "HAnimJoint", + "HAnimSegment", "HAnimSite", "head", "ImageTexture", "IMPORT", "IndexedFaceSet", + "IndexedLineSet", "IndexedTriangleFanSet", "IndexedTriangleSet", "IndexedTriangleStripSet", + "Inline", "IntegerSequencer", "IntegerTrigger", "IS", "KeySensor", "LineProperties", "LineSet", + "LoadSensor", "LOD", "Material", "meta", "MetadataDouble", "MetadataFloat", "MetadataInteger", + "MetadataSet", "MetadataString", "MovieTexture", "MultiTexture", "MultiTextureCoordinate", + "MultiTextureTransform", "NavigationInfo", "Normal", "NormalInterpolator", "NurbsCurve", + "NurbsCurve2D", "NurbsOrientationInterpolator", "NurbsPatchSurface", + "NurbsPositionInterpolator", "NurbsSet", "NurbsSurfaceInterpolator", "NurbsSweptSurface", + "NurbsSwungSurface", "NurbsTextureCoordinate", "NurbsTrimmedSurface", "OrientationInterpolator", + "PixelTexture", "PlaneSensor", "PointLight", "PointSet", "Polyline2D", "Polypoint2D", + "PositionInterpolator", "PositionInterpolator2D", "ProtoBody", "ProtoDeclare", "ProtoInstance", + "ProtoInterface", "ProximitySensor", "ReceiverPdu", "Rectangle2D", "ROUTE", "ScalarInterpolator", + "Scene", "Script", "Shape", "SignalPdu", "Sound", "Sphere", "SphereSensor", "SpotLight", "StaticGroup", + "StringSensor", "Switch", "Text", "TextureBackground", "TextureCoordinate", "TextureCoordinateGenerator", + "TextureTransform", "TimeSensor", "TimeTrigger", "TouchSensor", "Transform", "TransmitterPdu", + "TriangleFanSet", "TriangleSet", "TriangleSet2D", "TriangleStripSet", "Viewpoint", "VisibilitySensor", + "WorldInfo", "X3D", "XvlShell", "VertexShader", "FragmentShader", "MultiShaderAppearance", "ShaderAppearance") + + self.namesFog = ("", "LINEAR", "EXPONENTIAL", "") + +########################################################## +# Writing nodes routines +########################################################## + + def writeHeader(self): + #bfile = sys.expandpath( Blender.Get('filepath') ).replace('<', '<').replace('>', '>') + bfile = repr(os.path.basename(self.filepath).replace('<', '<').replace('>', '>'))[1:-1] # use outfile name + self.file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") + self.file.write("<!DOCTYPE X3D PUBLIC \"ISO//Web3D//DTD X3D 3.0//EN\" \"http://www.web3d.org/specifications/x3d-3.0.dtd\">\n") + self.file.write("<X3D version=\"3.0\" profile=\"Immersive\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema-instance\" xsd:noNamespaceSchemaLocation=\"http://www.web3d.org/specifications/x3d-3.0.xsd\">\n") + self.file.write("<head>\n") + self.file.write("\t<meta name=\"filename\" content=\"%s\" />\n" % bfile) + # self.file.write("\t<meta name=\"filename\" content=\"%s\" />\n" % sys.basename(bfile)) + self.file.write("\t<meta name=\"generator\" content=\"Blender %s\" />\n" % bpy.app.version_string) + # self.file.write("\t<meta name=\"generator\" content=\"Blender %s\" />\n" % Blender.Get('version')) + self.file.write("\t<meta name=\"translator\" content=\"X3D exporter v1.55 (2006/01/17)\" />\n") + self.file.write("</head>\n") + self.file.write("<Scene>\n") + + # This functionality is poorly defined, disabling for now - campbell + ''' + def writeScript(self): + textEditor = Blender.Text.Get() + alltext = len(textEditor) + for i in xrange(alltext): + nametext = textEditor[i].name + nlines = textEditor[i].getNLines() + if (self.proto == 1): + if (nametext == "proto" or nametext == "proto.js" or nametext == "proto.txt") and (nlines != None): + nalllines = len(textEditor[i].asLines()) + alllines = textEditor[i].asLines() + for j in xrange(nalllines): + self.write_indented(alllines[j] + "\n") + elif (self.proto == 0): + if (nametext == "route" or nametext == "route.js" or nametext == "route.txt") and (nlines != None): + nalllines = len(textEditor[i].asLines()) + alllines = textEditor[i].asLines() + for j in xrange(nalllines): + self.write_indented(alllines[j] + "\n") + self.write_indented("\n") + ''' + + def writeViewpoint(self, ob, mat, scene): + loc, quat, scale = mat.decompose() + self.file.write("<Viewpoint DEF=\"%s\" " % (self.cleanStr(ob.name))) + self.file.write("description=\"%s\" " % (ob.name)) + self.file.write("centerOfRotation=\"0 0 0\" ") + self.file.write("position=\"%3.2f %3.2f %3.2f\" " % loc[:]) + self.file.write("orientation=\"%3.2f %3.2f %3.2f %3.2f\" " % (quat.axis[:] + (quat.angle, ))) + self.file.write("fieldOfView=\"%.3f\" " % ob.data.angle) + self.file.write(" />\n\n") + + def writeFog(self, world): + if world: + mtype = world.mist_settings.falloff + mparam = world.mist_settings + else: + return + if (mtype == 'LINEAR' or mtype == 'INVERSE_QUADRATIC'): + mtype = 1 if mtype == 'LINEAR' else 2 + # if (mtype == 1 or mtype == 2): + self.file.write("<Fog fogType=\"%s\" " % self.namesFog[mtype]) + self.file.write("color=\"%s %s %s\" " % round_color(world.horizon_color, self.cp)) + self.file.write("visibilityRange=\"%s\" />\n\n" % round(mparam[2], self.cp)) + else: + return + + def writeNavigationInfo(self, scene): + self.file.write('<NavigationInfo headlight="false" visibilityLimit="0.0" type=\'"EXAMINE","ANY"\' avatarSize="0.25, 1.75, 0.75" />\n') + + def writeSpotLight(self, ob, mtx, lamp, world): + safeName = self.cleanStr(ob.name) + if world: + ambi = world.ambient_color + ambientIntensity = ((ambi[0] + ambi[1] + ambi[2]) / 3.0) / 2.5 + del ambi + else: + ambientIntensity = 0.0 + + # compute cutoff and beamwidth + intensity = min(lamp.energy / 1.75, 1.0) + beamWidth = lamp.spot_size * 0.37 + # beamWidth=((lamp.spotSize*math.pi)/180.0)*.37 + cutOffAngle = beamWidth * 1.3 + + dx, dy, dz = matrix_direction(mtx) + + location = mtx.translation_part() + + radius = lamp.distance * math.cos(beamWidth) + # radius = lamp.dist*math.cos(beamWidth) + self.file.write("<SpotLight DEF=\"%s\" " % safeName) + self.file.write("radius=\"%s\" " % (round(radius, self.cp))) + self.file.write("ambientIntensity=\"%s\" " % (round(ambientIntensity, self.cp))) + self.file.write("intensity=\"%s\" " % (round(intensity, self.cp))) + self.file.write("color=\"%s %s %s\" " % round_color(lamp.color, self.cp)) + self.file.write("beamWidth=\"%s\" " % (round(beamWidth, self.cp))) + self.file.write("cutOffAngle=\"%s\" " % (round(cutOffAngle, self.cp))) + self.file.write("direction=\"%s %s %s\" " % (round(dx, 3), round(dy, 3), round(dz, 3))) + self.file.write("location=\"%s %s %s\" />\n\n" % (round(location[0], 3), round(location[1], 3), round(location[2], 3))) + + def writeDirectionalLight(self, ob, mtx, lamp, world): + safeName = self.cleanStr(ob.name) + if world: + ambi = world.ambient_color + # ambi = world.amb + ambientIntensity = ((float(ambi[0] + ambi[1] + ambi[2])) / 3.0) / 2.5 + else: + ambi = 0 + ambientIntensity = 0 + + intensity = min(lamp.energy / 1.75, 1.0) + dx, dy, dz = matrix_direction(mtx) + self.file.write("<DirectionalLight DEF=\"%s\" " % safeName) + self.file.write("ambientIntensity=\"%s\" " % (round(ambientIntensity, self.cp))) + self.file.write("color=\"%s %s %s\" " % (round(lamp.color[0], self.cp), round(lamp.color[1], self.cp), round(lamp.color[2], self.cp))) + self.file.write("intensity=\"%s\" " % (round(intensity, self.cp))) + self.file.write("direction=\"%s %s %s\" />\n\n" % (round(dx, 4), round(dy, 4), round(dz, 4))) + + def writePointLight(self, ob, mtx, lamp, world): + safeName = self.cleanStr(ob.name) + if world: + ambi = world.ambient_color + # ambi = world.amb + ambientIntensity = ((float(ambi[0] + ambi[1] + ambi[2])) / 3) / 2.5 + else: + ambi = 0 + ambientIntensity = 0 + + location = mtx.translation_part() + + self.file.write("<PointLight DEF=\"%s\" " % safeName) + self.file.write("ambientIntensity=\"%s\" " % (round(ambientIntensity, self.cp))) + self.file.write("color=\"%s %s %s\" " % (round(lamp.color[0], self.cp), round(lamp.color[1], self.cp), round(lamp.color[2], self.cp))) + + self.file.write("intensity=\"%s\" " % (round(min(lamp.energy / 1.75, 1.0), self.cp))) + self.file.write("radius=\"%s\" " % lamp.distance) + self.file.write("location=\"%s %s %s\" />\n\n" % (round(location[0], 3), round(location[1], 3), round(location[2], 3))) + + def secureName(self, name): + name = name + str(self.nodeID) + self.nodeID = self.nodeID + 1 + if len(name) <= 3: + newname = "_" + str(self.nodeID) + return "%s" % (newname) + else: + for bad in ('"', '#', "'", ', ', '.', '[', '\\', ']', '{', '}'): + name = name.replace(bad, "_") + if name in self.namesReserved: + newname = name[0:3] + "_" + str(self.nodeID) + return "%s" % (newname) + elif name[0].isdigit(): + newname = "_" + name + str(self.nodeID) + return "%s" % (newname) + else: + newname = name + return "%s" % (newname) + + def writeIndexedFaceSet(self, ob, mesh, mtx, world, EXPORT_TRI=False): + fw = self.file.write + mesh_name_x3d = self.cleanStr(ob.name) + + if not mesh.faces: + return + + mode = [] + # mode = 0 + if mesh.uv_textures.active: + # if mesh.faceUV: + for face in mesh.uv_textures.active.data: + # for face in mesh.faces: + if face.use_halo and 'HALO' not in mode: + mode += ['HALO'] + if face.use_billboard and 'BILLBOARD' not in mode: + mode += ['BILLBOARD'] + if face.use_object_color and 'OBJECT_COLOR' not in mode: + mode += ['OBJECT_COLOR'] + if face.use_collision and 'COLLISION' not in mode: + mode += ['COLLISION'] + # mode |= face.mode + + if 'HALO' in mode and self.halonode == 0: + # if mode & Mesh.FaceModes.HALO and self.halonode == 0: + self.write_indented("<Billboard axisOfRotation=\"0 0 0\">\n", 1) + self.halonode = 1 + elif 'BILLBOARD' in mode and self.billnode == 0: + # elif mode & Mesh.FaceModes.BILLBOARD and self.billnode == 0: + self.write_indented("<Billboard axisOfRotation=\"0 1 0\">\n", 1) + self.billnode = 1 + elif 'COLLISION' not in mode and self.collnode == 0: + # elif not mode & Mesh.FaceModes.DYNAMIC and self.collnode == 0: + self.write_indented("<Collision enabled=\"false\">\n", 1) + self.collnode = 1 + + loc, quat, sca = mtx.decompose() + + self.write_indented("<Transform DEF=\"%s\" " % mesh_name_x3d, 1) + fw("translation=\"%.6f %.6f %.6f\" " % loc[:]) + fw("scale=\"%.6f %.6f %.6f\" " % sca[:]) + fw("rotation=\"%.6f %.6f %.6f %.6f\" " % (quat.axis[:] + (quat.angle, ))) + fw(">\n") + + if mesh.tag: + self.write_indented("<Group USE=\"G_%s\" />\n" % mesh_name_x3d, 1) + else: + mesh.tag = True + + self.write_indented("<Group DEF=\"G_%s\">\n" % mesh_name_x3d, 1) + + is_uv = bool(mesh.uv_textures.active) + # is_col, defined for each material + + is_coords_written = False + + mesh_materials = mesh.materials[:] + if not mesh_materials: + mesh_materials = [None] + + mesh_material_tex = [None] * len(mesh_materials) + mesh_material_mtex = [None] * len(mesh_materials) + mesh_material_images = [None] * len(mesh_materials) + + for i, material in enumerate(mesh_materials): + if material: + for mtex in material.texture_slots: + if mtex: + tex = mtex.texture + if tex and tex.type == 'IMAGE': + image = tex.image + if image: + mesh_material_tex[i] = tex + mesh_material_mtex[i] = mtex + mesh_material_images[i] = image + break + + mesh_materials_use_face_texture = [getattr(material, "use_face_texture", True) for material in mesh_materials] + + mesh_faces = mesh.faces[:] + mesh_faces_materials = [f.material_index for f in mesh_faces] + + if is_uv and True in mesh_materials_use_face_texture: + mesh_faces_image = [(fuv.image if (mesh_materials_use_face_texture[mesh_faces_materials[i]] and fuv.use_image) else mesh_material_images[mesh_faces_materials[i]]) for i, fuv in enumerate(mesh.uv_textures.active.data)] + mesh_faces_image_unique = set(mesh_faces_image) + elif len(set(mesh_material_images) | {None}) > 1: # make sure there is at least one image + mesh_faces_image = [mesh_material_images[material_index] for material_index in mesh_faces_materials] + mesh_faces_image_unique = set(mesh_faces_image) + else: + mesh_faces_image = [None] * len(mesh_faces) + mesh_faces_image_unique = {None} + + # group faces + face_groups = {} + for material_index in range(len(mesh_materials)): + for image in mesh_faces_image_unique: + face_groups[material_index, image] = [] + del mesh_faces_image_unique + + for i, (material_index, image) in enumerate(zip(mesh_faces_materials, mesh_faces_image)): + face_groups[material_index, image].append(i) + + for (material_index, image), face_group in face_groups.items(): + if face_group: + material = mesh_materials[material_index] + + self.write_indented("<Shape>\n", 1) + is_smooth = False + is_col = (mesh.vertex_colors.active and (material is None or material.use_vertex_color_paint)) + + # kludge but as good as it gets! + for i in face_group: + if mesh_faces[i].use_smooth: + is_smooth = True + break + + if image: + self.write_indented("<Appearance>\n", 1) + self.writeImageTexture(image) + + if mesh_materials_use_face_texture[material_index]: + if image.use_tiles: + self.write_indented("<TextureTransform scale=\"%s %s\" />\n" % (image.tiles_x, image.tiles_y)) + else: + # transform by mtex + loc = mesh_material_mtex[material_index].offset[:2] + + # mtex_scale * tex_repeat + sca_x, sca_y = mesh_material_mtex[material_index].scale[:2] + + sca_x *= mesh_material_tex[material_index].repeat_x + sca_y *= mesh_material_tex[material_index].repeat_y + + # flip x/y is a sampling feature, convert to transform + if mesh_material_tex[material_index].use_flip_axis: + rot = math.pi / -2.0 + sca_x, sca_y = sca_y, -sca_x + else: + rot = 0.0 + + self.write_indented("<TextureTransform ", 1) + # fw("center=\"%.6f %.6f\" " % (0.0, 0.0)) + fw("translation=\"%.6f %.6f\" " % loc) + fw("scale=\"%.6f %.6f\" " % (sca_x, sca_y)) + fw("rotation=\"%.6f\" " % rot) + fw("/>\n") + + self.write_indented("</Appearance>\n", -1) + + elif material: + self.write_indented("<Appearance>\n", 1) + self.writeMaterial(material, self.cleanStr(material.name, ""), world) + self.write_indented("</Appearance>\n", -1) + + #-- IndexedFaceSet or IndexedLineSet + + self.write_indented("<IndexedFaceSet ", 1) + + # --- Write IndexedFaceSet Attributes + if mesh.show_double_sided: + fw("solid=\"true\" ") + else: + fw("solid=\"false\" ") + + if is_smooth: + fw("creaseAngle=\"%.4f\" " % mesh.auto_smooth_angle) + + if is_uv: + # "texCoordIndex" + fw("\n\t\t\ttexCoordIndex=\"") + j = 0 + for i in face_group: + if len(mesh_faces[i].vertices) == 4: + fw("%d %d %d %d -1, " % (j, j + 1, j + 2, j + 3)) + j += 4 + else: + fw("%d %d %d -1, " % (j, j + 1, j + 2)) + j += 3 + fw("\" ") + # --- end texCoordIndex + + if is_col: + fw("colorPerVertex=\"false\" ") + + if True: + # "coordIndex" + fw('coordIndex="') + if EXPORT_TRI: + for i in face_group: + fv = mesh_faces[i].vertices[:] + if len(fv) == 3: + fw("%i %i %i -1, " % fv) + else: + fw("%i %i %i -1, " % (fv[0], fv[1], fv[2])) + fw("%i %i %i -1, " % (fv[0], fv[2], fv[3])) + else: + for i in face_group: + fv = mesh_faces[i].vertices[:] + if len(fv) == 3: + fw("%i %i %i -1, " % fv) + else: + fw("%i %i %i %i -1, " % fv) + + fw("\" ") + # --- end coordIndex + + # close IndexedFaceSet + fw(">\n") + + # --- Write IndexedFaceSet Elements + if True: + if is_coords_written: + self.write_indented("<Coordinate USE=\"%s%s\" />\n" % ("coord_", mesh_name_x3d)) + else: + self.write_indented("<Coordinate DEF=\"%s%s\" \n" % ("coord_", mesh_name_x3d), 1) + fw("\t\t\t\tpoint=\"") + for v in mesh.vertices: + fw("%.6f %.6f %.6f, " % v.co[:]) + fw("\" />") + self.write_indented("\n", -1) + is_coords_written = True + + if is_uv: + self.write_indented("<TextureCoordinate point=\"", 1) + fw = fw + mesh_faces_uv = mesh.uv_textures.active.data + for i in face_group: + for uv in mesh_faces_uv[i].uv: + fw("%.4f %.4f, " % uv[:]) + del mesh_faces_uv + fw("\" />") + self.write_indented("\n", -1) + + if is_col: + self.write_indented("<Color color=\"", 1) + # XXX, 1 color per face, only + mesh_faces_col = mesh.vertex_colors.active.data + for i in face_group: + fw("%.3f %.3f %.3f, " % mesh_faces_col[i].color1[:]) + del mesh_faces_col + fw("\" />") + self.write_indented("\n", -1) + + #--- output vertexColors + + #--- output closing braces + self.write_indented("</IndexedFaceSet>\n", -1) + self.write_indented("</Shape>\n", -1) + + self.write_indented("</Group>\n", -1) + + self.write_indented("</Transform>\n", -1) + + if self.halonode == 1: + self.write_indented("</Billboard>\n", -1) + self.halonode = 0 + + if self.billnode == 1: + self.write_indented("</Billboard>\n", -1) + self.billnode = 0 + + if self.collnode == 1: + self.write_indented("</Collision>\n", -1) + self.collnode = 0 + + fw("\n") + + def writeMaterial(self, mat, matName, world): + # look up material name, use it if available + if mat.tag: + self.write_indented("<Material USE=\"MA_%s\" />\n" % matName) + else: + mat.tag = True + + emit = mat.emit + ambient = mat.ambient / 3.0 + diffuseColor = tuple(mat.diffuse_color) + if world: + ambiColor = tuple(((c * mat.ambient) * 2.0) for c in world.ambient_color) + else: + ambiColor = 0.0, 0.0, 0.0 + + emitColor = tuple(((c * emit) + ambiColor[i]) / 2.0 for i, c in enumerate(diffuseColor)) + shininess = mat.specular_hardness / 512.0 + specColor = tuple((c + 0.001) / (1.25 / (mat.specular_intensity + 0.001)) for c in mat.specular_color) + transp = 1.0 - mat.alpha + + if mat.use_shadeless: + ambient = 1.0 + shininess = 0.0 + specColor = emitColor = diffuseColor + + self.write_indented("<Material DEF=\"MA_%s\" " % matName, 1) + self.file.write("diffuseColor=\"%s %s %s\" " % round_color(diffuseColor, self.cp)) + self.file.write("specularColor=\"%s %s %s\" " % round_color(specColor, self.cp)) + self.file.write("emissiveColor=\"%s %s %s\" \n" % round_color(emitColor, self.cp)) + self.write_indented("ambientIntensity=\"%s\" " % (round(ambient, self.cp))) + self.file.write("shininess=\"%s\" " % (round(shininess, self.cp))) + self.file.write("transparency=\"%s\" />" % (round(transp, self.cp))) + self.write_indented("\n", -1) + + def writeImageTexture(self, image): + name = image.name + filepath = os.path.basename(image.filepath) + if image.tag: + self.write_indented("<ImageTexture USE=\"%s\" />\n" % self.cleanStr(name)) + else: + image.tag = True + + self.write_indented("<ImageTexture DEF=\"%s\" " % self.cleanStr(name), 1) + self.file.write("url=\"%s\" />" % filepath) + self.write_indented("\n", -1) + + def writeBackground(self, world, alltextures): + if world: + worldname = world.name + else: + return + + blending = world.use_sky_blend, world.use_sky_paper, world.use_sky_real + + grd_triple = round_color(world.horizon_color, self.cp) + sky_triple = round_color(world.zenith_color, self.cp) + mix_triple = round_color(((grd_triple[i] + sky_triple[i]) / 2.0 for i in range(3)), self.cp) + + self.file.write("<Background DEF=\"%s\" " % self.secureName(worldname)) + # No Skytype - just Hor color + if blending == (False, False, False): + self.file.write("groundColor=\"%s %s %s\" " % grd_triple) + self.file.write("skyColor=\"%s %s %s\" " % grd_triple) + # Blend Gradient + elif blending == (True, False, False): + self.file.write("groundColor=\"%s %s %s, " % grd_triple) + self.file.write("%s %s %s\" groundAngle=\"1.57, 1.57\" " % mix_triple) + self.file.write("skyColor=\"%s %s %s, " % sky_triple) + self.file.write("%s %s %s\" skyAngle=\"1.57, 1.57\" " % mix_triple) + # Blend+Real Gradient Inverse + elif blending == (True, False, True): + self.file.write("groundColor=\"%s %s %s, " % sky_triple) + self.file.write("%s %s %s\" groundAngle=\"1.57, 1.57\" " % mix_triple) + self.file.write("skyColor=\"%s %s %s, " % grd_triple) + self.file.write("%s %s %s\" skyAngle=\"1.57, 1.57\" " % mix_triple) + # Paper - just Zen Color + elif blending == (False, False, True): + self.file.write("groundColor=\"%s %s %s\" " % sky_triple) + self.file.write("skyColor=\"%s %s %s\" " % sky_triple) + # Blend+Real+Paper - komplex gradient + elif blending == (True, True, True): + self.write_indented("groundColor=\"%s %s %s, " % sky_triple) + self.write_indented("%s %s %s\" groundAngle=\"1.57, 1.57\" " % grd_triple) + self.write_indented("skyColor=\"%s %s %s, " % sky_triple) + self.write_indented("%s %s %s\" skyAngle=\"1.57, 1.57\" " % grd_triple) + # Any Other two colors + else: + self.file.write("groundColor=\"%s %s %s\" " % grd_triple) + self.file.write("skyColor=\"%s %s %s\" " % sky_triple) + + alltexture = len(alltextures) + + for i in range(alltexture): + tex = alltextures[i] + + if tex.type != 'IMAGE' or tex.image is None: + continue + + namemat = tex.name + # namemat = alltextures[i].name + + pic = tex.image + + # using .expandpath just in case, os.path may not expect // + basename = os.path.basename(bpy.path.abspath(pic.filepath)) + + pic = alltextures[i].image + if (namemat == "back") and (pic != None): + self.file.write("\n\tbackUrl=\"%s\" " % basename) + elif (namemat == "bottom") and (pic != None): + self.write_indented("bottomUrl=\"%s\" " % basename) + elif (namemat == "front") and (pic != None): + self.write_indented("frontUrl=\"%s\" " % basename) + elif (namemat == "left") and (pic != None): + self.write_indented("leftUrl=\"%s\" " % basename) + elif (namemat == "right") and (pic != None): + self.write_indented("rightUrl=\"%s\" " % basename) + elif (namemat == "top") and (pic != None): + self.write_indented("topUrl=\"%s\" " % basename) + self.write_indented("/>\n\n") + +########################################################## +# export routine +########################################################## + + def export(self, scene, world, alltextures, + EXPORT_APPLY_MODIFIERS=False, + EXPORT_TRI=False, + ): + + # tag un-exported IDs + bpy.data.meshes.tag(False) + bpy.data.materials.tag(False) + bpy.data.images.tag(False) + + print("Info: starting X3D export to %r..." % self.filepath) + self.writeHeader() + # self.writeScript() + self.writeNavigationInfo(scene) + self.writeBackground(world, alltextures) + self.writeFog(world) + self.proto = 0 + + for ob_main in [o for o in scene.objects if o.is_visible(scene)]: + + free, derived = create_derived_objects(scene, ob_main) + + if derived is None: + continue + + for ob, ob_mat in derived: + objType = ob.type + objName = ob.name + ob_mat = self.global_matrix * ob_mat + + if objType == 'CAMERA': + self.writeViewpoint(ob, ob_mat, scene) + elif objType in ('MESH', 'CURVE', 'SURF', 'FONT'): + if EXPORT_APPLY_MODIFIERS or objType != 'MESH': + me = ob.create_mesh(scene, EXPORT_APPLY_MODIFIERS, 'PREVIEW') + else: + me = ob.data + + self.writeIndexedFaceSet(ob, me, ob_mat, world, EXPORT_TRI=EXPORT_TRI) + + # free mesh created with create_mesh() + if me != ob.data: + bpy.data.meshes.remove(me) + + elif objType == 'LAMP': + data = ob.data + datatype = data.type + if datatype == 'POINT': + self.writePointLight(ob, ob_mat, data, world) + elif datatype == 'SPOT': + self.writeSpotLight(ob, ob_mat, data, world) + elif datatype == 'SUN': + self.writeDirectionalLight(ob, ob_mat, data, world) + else: + self.writeDirectionalLight(ob, ob_mat, data, world) + else: + #print "Info: Ignoring [%s], object type [%s] not handle yet" % (object.name,object.getType) + pass + + if free: + free_derived_objects(ob_main) + + self.file.write("\n</Scene>\n</X3D>") + + # if EXPORT_APPLY_MODIFIERS: + # if containerMesh: + # containerMesh.vertices = None + + self.cleanup() + +########################################################## +# Utility methods +########################################################## + + def cleanup(self): + self.file.close() + self.indentLevel = 0 + print("Info: finished X3D export to %r" % self.filepath) + + def cleanStr(self, name, prefix='rsvd_'): + """cleanStr(name,prefix) - try to create a valid VRML DEF name from object name""" + + newName = name + if len(newName) == 0: + self.nNodeID += 1 + return "%s%d" % (prefix, self.nNodeID) + + if newName in self.namesReserved: + newName = '%s%s' % (prefix, newName) + + if newName[0].isdigit(): + newName = "%s%s" % ('_', newName) + + for bad in [' ', '"', '#', "'", ', ', '.', '[', '\\', ']', '{', '}']: + newName = newName.replace(bad, '_') + return newName + + def faceToString(self, face): + + print("Debug: face.flag=0x%x (bitflags)" % face.flag) + if face.sel: + print("Debug: face.sel=true") + + print("Debug: face.mode=0x%x (bitflags)" % face.mode) + if face.mode & Mesh.FaceModes.TWOSIDE: + print("Debug: face.mode twosided") + + print("Debug: face.transp=0x%x (enum)" % face.blend_type) + if face.blend_type == Mesh.FaceTranspModes.SOLID: + print("Debug: face.transp.SOLID") + + if face.image: + print("Debug: face.image=%s" % face.image.name) + print("Debug: face.materialIndex=%d" % face.materialIndex) + + def meshToString(self, mesh): + # print("Debug: mesh.hasVertexUV=%d" % mesh.vertexColors) + print("Debug: mesh.faceUV=%d" % (len(mesh.uv_textures) > 0)) + # print("Debug: mesh.faceUV=%d" % mesh.faceUV) + print("Debug: mesh.hasVertexColours=%d" % (len(mesh.vertex_colors) > 0)) + # print("Debug: mesh.hasVertexColours=%d" % mesh.hasVertexColours()) + print("Debug: mesh.vertices=%d" % len(mesh.vertices)) + print("Debug: mesh.faces=%d" % len(mesh.faces)) + print("Debug: mesh.materials=%d" % len(mesh.materials)) + + # s="%s %s %s" % ( + # round(c.r/255.0,self.cp), + # round(c.g/255.0,self.cp), + # round(c.b/255.0,self.cp)) + return s + + # For writing well formed VRML code + #------------------------------------------------------------------------ + def write_indented(self, s, inc=0): + if inc < 1: + self.indentLevel = self.indentLevel + inc + + self.file.write((self.indentLevel * "\t") + s) + + if inc > 0: + self.indentLevel = self.indentLevel + inc + +########################################################## +# Callbacks, needed before Main +########################################################## + + +def save(operator, context, filepath="", + use_apply_modifiers=False, + use_triangulate=False, + use_compress=False): + + if use_compress: + if not filepath.lower().endswith('.x3dz'): + filepath = '.'.join(filepath.split('.')[:-1]) + '.x3dz' + else: + if not filepath.lower().endswith('.x3d'): + filepath = '.'.join(filepath.split('.')[:-1]) + '.x3d' + + scene = context.scene + world = scene.world + + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode='OBJECT') + + # XXX these are global textures while .Get() returned only scene's? + alltextures = bpy.data.textures + # alltextures = Blender.Texture.Get() + + wrlexport = x3d_class(filepath) + wrlexport.export(scene, + world, + alltextures, + EXPORT_APPLY_MODIFIERS=use_apply_modifiers, + EXPORT_TRI=use_triangulate, + ) + + return {'FINISHED'} diff --git a/io_scene_x3d/import_x3d.py b/io_scene_x3d/import_x3d.py new file mode 100644 index 00000000..f2885943 --- /dev/null +++ b/io_scene_x3d/import_x3d.py @@ -0,0 +1,2658 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +DEBUG = False + +# This should work without a blender at all +from os.path import exists + + +def baseName(path): + return path.split('/')[-1].split('\\')[-1] + + +def dirName(path): + return path[:-len(baseName(path))] + + +def imageConvertCompat(path): + + try: + import os + except: + return path + if os.sep == '\\': + return path # assime win32 has quicktime, dont convert + + if path.lower().endswith('.gif'): + path_to = path[:-3] + 'png' + + ''' + if exists(path_to): + return path_to + ''' + # print('\n'+path+'\n'+path_to+'\n') + os.system('convert "%s" "%s"' % (path, path_to)) # for now just hope we have image magick + + if exists(path_to): + return path_to + + return path + +# notes +# transform are relative +# order dosnt matter for loc/size/rot +# right handed rotation +# angles are in radians +# rotation first defines axis then ammount in radians + + +# =============================== VRML Spesific + +def vrmlFormat(data): + ''' + Keep this as a valid vrml file, but format in a way we can predict. + ''' + # Strip all commends - # not in strings - warning multiline strings are ignored. + def strip_comment(l): + #l = ' '.join(l.split()) + l = l.strip() + + if l.startswith('#'): + return '' + + i = l.find('#') + + if i == -1: + return l + + # Most cases accounted for! if we have a comment at the end of the line do this... + #j = l.find('url "') + j = l.find('"') + + if j == -1: # simple no strings + return l[:i].strip() + + q = False + for i, c in enumerate(l): + if c == '"': + q = not q # invert + + elif c == '#': + if q == False: + return l[:i - 1] + + return l + + data = '\n'.join([strip_comment(l) for l in data.split('\n')]) # remove all whitespace + + EXTRACT_STRINGS = True # only needed when strings or filesnames containe ,[]{} chars :/ + + if EXTRACT_STRINGS: + + # We need this so we can detect URL's + data = '\n'.join([' '.join(l.split()) for l in data.split('\n')]) # remove all whitespace + + string_ls = [] + + #search = 'url "' + search = '"' + + ok = True + last_i = 0 + while ok: + ok = False + i = data.find(search, last_i) + if i != -1: + + start = i + len(search) # first char after end of search + end = data.find('"', start) + if end != -1: + item = data[start:end] + string_ls.append(item) + data = data[:start] + data[end:] + ok = True # keep looking + + last_i = (end - len(item)) + 1 + # print(last_i, item, '|' + data[last_i] + '|') + + # done with messy extracting strings part + + # Bad, dont take strings into account + ''' + data = data.replace('#', '\n#') + data = '\n'.join([ll for l in data.split('\n') for ll in (l.strip(),) if not ll.startswith('#')]) # remove all whitespace + ''' + data = data.replace('{', '\n{\n') + data = data.replace('}', '\n}\n') + data = data.replace('[', '\n[\n') + data = data.replace(']', '\n]\n') + data = data.replace(',', ' , ') # make sure comma's seperate + + if EXTRACT_STRINGS: + # add strings back in + + search = '"' # fill in these empty strings + + ok = True + last_i = 0 + while ok: + ok = False + i = data.find(search + '"', last_i) + # print(i) + if i != -1: + start = i + len(search) # first char after end of search + item = string_ls.pop(0) + # print(item) + data = data[:start] + item + data[start:] + + last_i = start + len(item) + 1 + + ok = True + + # More annoying obscure cases where USE or DEF are placed on a newline + # data = data.replace('\nDEF ', ' DEF ') + # data = data.replace('\nUSE ', ' USE ') + + data = '\n'.join([' '.join(l.split()) for l in data.split('\n')]) # remove all whitespace + + # Better to parse the file accounting for multiline arrays + ''' + data = data.replace(',\n', ' , ') # remove line endings with commas + data = data.replace(']', '\n]\n') # very very annoying - but some comma's are at the end of the list, must run this again. + ''' + + return [l for l in data.split('\n') if l] + +NODE_NORMAL = 1 # {} +NODE_ARRAY = 2 # [] +NODE_REFERENCE = 3 # USE foobar +# NODE_PROTO = 4 # + +lines = [] + + +def getNodePreText(i, words): + # print(lines[i]) + use_node = False + while len(words) < 5: + + if i >= len(lines): + break + ''' + elif lines[i].startswith('PROTO'): + return NODE_PROTO, i+1 + ''' + elif lines[i] == '{': + # words.append(lines[i]) # no need + # print("OK") + return NODE_NORMAL, i + 1 + elif lines[i].count('"') % 2 != 0: # odd number of quotes? - part of a string. + # print('ISSTRING') + break + else: + new_words = lines[i].split() + if 'USE' in new_words: + use_node = True + + words.extend(new_words) + i += 1 + + # Check for USE node - no { + # USE #id - should always be on the same line. + if use_node: + # print('LINE', i, words[:words.index('USE')+2]) + words[:] = words[:words.index('USE') + 2] + if lines[i] == '{' and lines[i + 1] == '}': + # USE sometimes has {} after it anyway + i += 2 + return NODE_REFERENCE, i + + # print("error value!!!", words) + return 0, -1 + + +def is_nodeline(i, words): + + if not lines[i][0].isalpha(): + return 0, 0 + + #if lines[i].startswith('field'): + # return 0, 0 + + # Is this a prototype?? + if lines[i].startswith('PROTO'): + words[:] = lines[i].split() + return NODE_NORMAL, i + 1 # TODO - assumes the next line is a '[\n', skip that + if lines[i].startswith('EXTERNPROTO'): + words[:] = lines[i].split() + return NODE_ARRAY, i + 1 # TODO - assumes the next line is a '[\n', skip that + + ''' + proto_type, new_i = is_protoline(i, words, proto_field_defs) + if new_i != -1: + return proto_type, new_i + ''' + + # Simple "var [" type + if lines[i + 1] == '[': + if lines[i].count('"') % 2 == 0: + words[:] = lines[i].split() + return NODE_ARRAY, i + 2 + + node_type, new_i = getNodePreText(i, words) + + if not node_type: + if DEBUG: + print("not node_type", lines[i]) + return 0, 0 + + # Ok, we have a { after some values + # Check the values are not fields + for i, val in enumerate(words): + if i != 0 and words[i - 1] in ('DEF', 'USE'): + # ignore anything after DEF, it is a ID and can contain any chars. + pass + elif val[0].isalpha() and val not in ('TRUE', 'FALSE'): + pass + else: + # There is a number in one of the values, therefor we are not a node. + return 0, 0 + + #if node_type==NODE_REFERENCE: + # print(words, "REF_!!!!!!!") + return node_type, new_i + + +def is_numline(i): + ''' + Does this line start with a number? + ''' + + # Works but too slow. + ''' + l = lines[i] + for w in l.split(): + if w==',': + pass + else: + try: + float(w) + return True + + except: + return False + + return False + ''' + + l = lines[i] + + line_start = 0 + + if l.startswith(', '): + line_start += 2 + + line_end = len(l) - 1 + line_end_new = l.find(' ', line_start) # comma's always have a space before them + + if line_end_new != -1: + line_end = line_end_new + + try: + float(l[line_start:line_end]) # works for a float or int + return True + except: + return False + + +class vrmlNode(object): + __slots__ = ('id', + 'fields', + 'proto_node', + 'proto_field_defs', + 'proto_fields', + 'node_type', + 'parent', + 'children', + 'parent', + 'array_data', + 'reference', + 'lineno', + 'filename', + 'blendObject', + 'DEF_NAMESPACE', + 'ROUTE_IPO_NAMESPACE', + 'PROTO_NAMESPACE', + 'x3dNode') + + def __init__(self, parent, node_type, lineno): + self.id = None + self.node_type = node_type + self.parent = parent + self.blendObject = None + self.x3dNode = None # for x3d import only + if parent: + parent.children.append(self) + + self.lineno = lineno + + # This is only set from the root nodes. + # Having a filename also denotes a root node + self.filename = None + self.proto_node = None # proto field definition eg: "field SFColor seatColor .6 .6 .1" + + # Store in the root node because each inline file needs its own root node and its own namespace + self.DEF_NAMESPACE = None + self.ROUTE_IPO_NAMESPACE = None + ''' + self.FIELD_NAMESPACE = None + ''' + + self.PROTO_NAMESPACE = None + + self.reference = None + + if node_type == NODE_REFERENCE: + # For references, only the parent and ID are needed + # the reference its self is assigned on parsing + return + + self.fields = [] # fields have no order, in some cases rool level values are not unique so dont use a dict + + self.proto_field_defs = [] # proto field definition eg: "field SFColor seatColor .6 .6 .1" + self.proto_fields = [] # proto field usage "diffuseColor IS seatColor" + self.children = [] + self.array_data = [] # use for arrays of data - should only be for NODE_ARRAY types + + # Only available from the root node + ''' + def getFieldDict(self): + if self.FIELD_NAMESPACE != None: + return self.FIELD_NAMESPACE + else: + return self.parent.getFieldDict() + ''' + def getProtoDict(self): + if self.PROTO_NAMESPACE != None: + return self.PROTO_NAMESPACE + else: + return self.parent.getProtoDict() + + def getDefDict(self): + if self.DEF_NAMESPACE != None: + return self.DEF_NAMESPACE + else: + return self.parent.getDefDict() + + def getRouteIpoDict(self): + if self.ROUTE_IPO_NAMESPACE != None: + return self.ROUTE_IPO_NAMESPACE + else: + return self.parent.getRouteIpoDict() + + def setRoot(self, filename): + self.filename = filename + # self.FIELD_NAMESPACE = {} + self.DEF_NAMESPACE = {} + self.ROUTE_IPO_NAMESPACE = {} + self.PROTO_NAMESPACE = {} + + def isRoot(self): + if self.filename == None: + return False + else: + return True + + def getFilename(self): + if self.filename: + return self.filename + elif self.parent: + return self.parent.getFilename() + else: + return None + + def getRealNode(self): + if self.reference: + return self.reference + else: + return self + + def getSpec(self): + self_real = self.getRealNode() + try: + return self_real.id[-1] # its possible this node has no spec + except: + return None + + def findSpecRecursive(self, spec): + self_real = self.getRealNode() + if spec == self_real.getSpec(): + return self + + for child in self_real.children: + if child.findSpecRecursive(spec): + return child + + return None + + def getPrefix(self): + if self.id: + return self.id[0] + return None + + def getSpecialTypeName(self, typename): + self_real = self.getRealNode() + try: + return self_real.id[list(self_real.id).index(typename) + 1] + except: + return None + + def getDefName(self): + return self.getSpecialTypeName('DEF') + + def getProtoName(self): + return self.getSpecialTypeName('PROTO') + + def getExternprotoName(self): + return self.getSpecialTypeName('EXTERNPROTO') + + def getChildrenBySpec(self, node_spec): # spec could be Transform, Shape, Appearance + self_real = self.getRealNode() + # using getSpec functions allows us to use the spec of USE children that dont have their spec in their ID + if type(node_spec) == str: + return [child for child in self_real.children if child.getSpec() == node_spec] + else: + # Check inside a list of optional types + return [child for child in self_real.children if child.getSpec() in node_spec] + + def getChildBySpec(self, node_spec): # spec could be Transform, Shape, Appearance + # Use in cases where there is only ever 1 child of this type + ls = self.getChildrenBySpec(node_spec) + if ls: + return ls[0] + else: + return None + + def getChildrenByName(self, node_name): # type could be geometry, children, appearance + self_real = self.getRealNode() + return [child for child in self_real.children if child.id if child.id[0] == node_name] + + def getChildByName(self, node_name): + self_real = self.getRealNode() + for child in self_real.children: + if child.id and child.id[0] == node_name: # and child.id[-1]==node_spec: + return child + + def getSerialized(self, results, ancestry): + ''' Return this node and all its children in a flat list ''' + ancestry = ancestry[:] # always use a copy + + # self_real = self.getRealNode() + + results.append((self, tuple(ancestry))) + ancestry.append(self) + for child in self.getRealNode().children: + if child not in ancestry: + # We dont want to load proto's, they are only references + # We could enforce this elsewhere + + # Only add this in a very special case + # where the parent of this object is not the real parent + # - In this case we have added the proto as a child to a node instancing it. + # This is a bit arbitary, but its how Proto's are done with this importer. + if child.getProtoName() == None and child.getExternprotoName() == None: + child.getSerialized(results, ancestry) + else: + + if DEBUG: + print('getSerialized() is proto:', child.getProtoName(), child.getExternprotoName(), self.getSpec()) + + self_spec = self.getSpec() + + if child.getProtoName() == self_spec or child.getExternprotoName() == self_spec: + #if DEBUG: + # "FoundProto!" + child.getSerialized(results, ancestry) + + return results + + def searchNodeTypeID(self, node_spec, results): + self_real = self.getRealNode() + # print(self.lineno, self.id) + if self_real.id and self_real.id[-1] == node_spec: # use last element, could also be only element + results.append(self_real) + for child in self_real.children: + child.searchNodeTypeID(node_spec, results) + return results + + def getFieldName(self, field, ancestry, AS_CHILD=False): + self_real = self.getRealNode() # incase we're an instance + + for f in self_real.fields: + # print(f) + if f and f[0] == field: + # print('\tfound field', f) + + if len(f) >= 3 and f[1] == 'IS': # eg: 'diffuseColor IS legColor' + field_id = f[2] + + # print("\n\n\n\n\n\nFOND IS!!!") + f_proto_lookup = None + f_proto_child_lookup = None + i = len(ancestry) + while i: + i -= 1 + node = ancestry[i] + node = node.getRealNode() + + # proto settings are stored in "self.proto_node" + if node.proto_node: + # Get the default value from the proto, this can be overwridden by the proto instace + # 'field SFColor legColor .8 .4 .7' + if AS_CHILD: + for child in node.proto_node.children: + #if child.id and len(child.id) >= 3 and child.id[2]==field_id: + if child.id and ('point' in child.id or 'points' in child.id): + f_proto_child_lookup = child + + else: + for f_def in node.proto_node.proto_field_defs: + if len(f_def) >= 4: + if f_def[0] == 'field' and f_def[2] == field_id: + f_proto_lookup = f_def[3:] + + # Node instance, Will be 1 up from the proto-node in the ancestry list. but NOT its parent. + # This is the setting as defined by the instance, including this setting is optional, + # and will override the default PROTO value + # eg: 'legColor 1 0 0' + if AS_CHILD: + for child in node.children: + if child.id and child.id[0] == field_id: + f_proto_child_lookup = child + else: + for f_def in node.fields: + if len(f_def) >= 2: + if f_def[0] == field_id: + if DEBUG: + print("getFieldName(), found proto", f_def) + f_proto_lookup = f_def[1:] + + if AS_CHILD: + if f_proto_child_lookup: + if DEBUG: + print("getFieldName() - AS_CHILD=True, child found") + print(f_proto_child_lookup) + return f_proto_child_lookup + else: + return f_proto_lookup + else: + if AS_CHILD: + return None + else: + # Not using a proto + return f[1:] + # print('\tfield not found', field) + + # See if this is a proto name + if AS_CHILD: + child_array = None + for child in self_real.children: + if child.id and len(child.id) == 1 and child.id[0] == field: + return child + + return None + + def getFieldAsInt(self, field, default, ancestry): + self_real = self.getRealNode() # incase we're an instance + + f = self_real.getFieldName(field, ancestry) + if f == None: + return default + if ',' in f: + f = f[:f.index(',')] # strip after the comma + + if len(f) != 1: + print('\t"%s" wrong length for int conversion for field "%s"' % (f, field)) + return default + + try: + return int(f[0]) + except: + print('\tvalue "%s" could not be used as an int for field "%s"' % (f[0], field)) + return default + + def getFieldAsFloat(self, field, default, ancestry): + self_real = self.getRealNode() # incase we're an instance + + f = self_real.getFieldName(field, ancestry) + if f == None: + return default + if ',' in f: + f = f[:f.index(',')] # strip after the comma + + if len(f) != 1: + print('\t"%s" wrong length for float conversion for field "%s"' % (f, field)) + return default + + try: + return float(f[0]) + except: + print('\tvalue "%s" could not be used as a float for field "%s"' % (f[0], field)) + return default + + def getFieldAsFloatTuple(self, field, default, ancestry): + self_real = self.getRealNode() # incase we're an instance + + f = self_real.getFieldName(field, ancestry) + if f == None: + return default + # if ',' in f: f = f[:f.index(',')] # strip after the comma + + if len(f) < 1: + print('"%s" wrong length for float tuple conversion for field "%s"' % (f, field)) + return default + + ret = [] + for v in f: + if v != ',': + try: + ret.append(float(v)) + except: + break # quit of first non float, perhaps its a new field name on the same line? - if so we are going to ignore it :/ TODO + # print(ret) + + if ret: + return ret + if not ret: + print('\tvalue "%s" could not be used as a float tuple for field "%s"' % (f, field)) + return default + + def getFieldAsBool(self, field, default, ancestry): + self_real = self.getRealNode() # incase we're an instance + + f = self_real.getFieldName(field, ancestry) + if f == None: + return default + if ',' in f: + f = f[:f.index(',')] # strip after the comma + + if len(f) != 1: + print('\t"%s" wrong length for bool conversion for field "%s"' % (f, field)) + return default + + if f[0].upper() == '"TRUE"' or f[0].upper() == 'TRUE': + return True + elif f[0].upper() == '"FALSE"' or f[0].upper() == 'FALSE': + return False + else: + print('\t"%s" could not be used as a bool for field "%s"' % (f[1], field)) + return default + + def getFieldAsString(self, field, default, ancestry): + self_real = self.getRealNode() # incase we're an instance + + f = self_real.getFieldName(field, ancestry) + if f == None: + return default + if len(f) < 1: + print('\t"%s" wrong length for string conversion for field "%s"' % (f, field)) + return default + + if len(f) > 1: + # String may contain spaces + st = ' '.join(f) + else: + st = f[0] + + # X3D HACK + if self.x3dNode: + return st + + if st[0] == '"' and st[-1] == '"': + return st[1:-1] + else: + print('\tvalue "%s" could not be used as a string for field "%s"' % (f[0], field)) + return default + + def getFieldAsArray(self, field, group, ancestry): + ''' + For this parser arrays are children + ''' + + def array_as_number(array_string): + array_data = [] + try: + array_data = [int(val) for val in array_string] + except: + try: + array_data = [float(val) for val in array_string] + except: + print('\tWarning, could not parse array data from field') + + return array_data + + self_real = self.getRealNode() # incase we're an instance + + child_array = self_real.getFieldName(field, ancestry, True) + + #if type(child_array)==list: # happens occasionaly + # array_data = child_array + + if child_array is None: + # For x3d, should work ok with vrml too + # for x3d arrays are fields, vrml they are nodes, annoying but not tooo bad. + data_split = self.getFieldName(field, ancestry) + if not data_split: + return [] + array_data = ' '.join(data_split) + if array_data == None: + return [] + + array_data = array_data.replace(',', ' ') + data_split = array_data.split() + + array_data = array_as_number(data_split) + + elif type(child_array) == list: + # x3d creates these + data_split = [w.strip(",") for w in child_array] + + array_data = array_as_number(data_split) + else: + # print(child_array) + # Normal vrml + array_data = child_array.array_data + + # print('array_data', array_data) + if group == -1 or len(array_data) == 0: + return array_data + + # We want a flat list + flat = True + for item in array_data: + if type(item) == list: + flat = False + break + + # make a flat array + if flat: + flat_array = array_data # we are alredy flat. + else: + flat_array = [] + + def extend_flat(ls): + for item in ls: + if type(item) == list: + extend_flat(item) + else: + flat_array.append(item) + + extend_flat(array_data) + + # We requested a flat array + if group == 0: + return flat_array + + new_array = [] + sub_array = [] + + for item in flat_array: + sub_array.append(item) + if len(sub_array) == group: + new_array.append(sub_array) + sub_array = [] + + if sub_array: + print('\twarning, array was not aligned to requested grouping', group, 'remaining value', sub_array) + + return new_array + + def getFieldAsStringArray(self, field, ancestry): + ''' + Get a list of strings + ''' + self_real = self.getRealNode() # incase we're an instance + + child_array = None + for child in self_real.children: + if child.id and len(child.id) == 1 and child.id[0] == field: + child_array = child + break + if not child_array: + return [] + + # each string gets its own list, remove ""'s + try: + new_array = [f[0][1:-1] for f in child_array.fields] + except: + print('\twarning, string array could not be made') + new_array = [] + + return new_array + + def getLevel(self): + # Ignore self_real + level = 0 + p = self.parent + while p: + level += 1 + p = p.parent + if not p: + break + + return level + + def __repr__(self): + level = self.getLevel() + ind = ' ' * level + if self.node_type == NODE_REFERENCE: + brackets = '' + elif self.node_type == NODE_NORMAL: + brackets = '{}' + else: + brackets = '[]' + + if brackets: + text = ind + brackets[0] + '\n' + else: + text = '' + + text += ind + 'ID: ' + str(self.id) + ' ' + str(level) + (' lineno %d\n' % self.lineno) + + if self.node_type == NODE_REFERENCE: + text += ind + "(reference node)\n" + return text + + if self.proto_node: + text += ind + 'PROTO NODE...\n' + text += str(self.proto_node) + text += ind + 'PROTO NODE_DONE\n' + + text += ind + 'FIELDS:' + str(len(self.fields)) + '\n' + + for i, item in enumerate(self.fields): + text += ind + 'FIELD:\n' + text += ind + str(item) + '\n' + + text += ind + 'PROTO_FIELD_DEFS:' + str(len(self.proto_field_defs)) + '\n' + + for i, item in enumerate(self.proto_field_defs): + text += ind + 'PROTO_FIELD:\n' + text += ind + str(item) + '\n' + + text += ind + 'ARRAY: ' + str(len(self.array_data)) + ' ' + str(self.array_data) + '\n' + #text += ind + 'ARRAY: ' + str(len(self.array_data)) + '[...] \n' + + text += ind + 'CHILDREN: ' + str(len(self.children)) + '\n' + for i, child in enumerate(self.children): + text += ind + ('CHILD%d:\n' % i) + text += str(child) + + text += '\n' + ind + brackets[1] + + return text + + def parse(self, i, IS_PROTO_DATA=False): + new_i = self.__parse(i, IS_PROTO_DATA) + + # print(self.id, self.getFilename()) + + # Check if this node was an inline or externproto + + url_ls = [] + + if self.node_type == NODE_NORMAL and self.getSpec() == 'Inline': + ancestry = [] # Warning! - PROTO's using this wont work at all. + url = self.getFieldAsString('url', None, ancestry) + if url: + url_ls = [(url, None)] + del ancestry + + elif self.getExternprotoName(): + # externproto + url_ls = [] + for f in self.fields: + + if type(f) == str: + f = [f] + + for ff in f: + for f_split in ff.split('"'): + # print(f_split) + # "someextern.vrml#SomeID" + if '#' in f_split: + + f_split, f_split_id = f_split.split('#') # there should only be 1 # anyway + + url_ls.append((f_split, f_split_id)) + else: + url_ls.append((f_split, None)) + + # Was either an Inline or an EXTERNPROTO + if url_ls: + + # print(url_ls) + + for url, extern_key in url_ls: + print(url) + urls = [] + urls.append(url) + urls.append(bpy.path.resolve_ncase(urls[-1])) + + urls.append(dirName(self.getFilename()) + url) + urls.append(bpy.path.resolve_ncase(urls[-1])) + + urls.append(dirName(self.getFilename()) + baseName(url)) + urls.append(bpy.path.resolve_ncase(urls[-1])) + + try: + url = [url for url in urls if exists(url)][0] + url_found = True + except: + url_found = False + + if not url_found: + print('\tWarning: Inline URL could not be found:', url) + else: + if url == self.getFilename(): + print('\tWarning: cant Inline yourself recursively:', url) + else: + + try: + data = gzipOpen(url) + except: + print('\tWarning: cant open the file:', url) + data = None + + if data: + # Tricky - inline another VRML + print('\tLoading Inline:"%s"...' % url) + + # Watch it! - backup lines + lines_old = lines[:] + + lines[:] = vrmlFormat(data) + + lines.insert(0, '{') + lines.insert(0, 'root_node____') + lines.append('}') + ''' + ff = open('/tmp/test.txt', 'w') + ff.writelines([l+'\n' for l in lines]) + ''' + + child = vrmlNode(self, NODE_NORMAL, -1) + child.setRoot(url) # initialized dicts + child.parse(0) + + # if self.getExternprotoName(): + if self.getExternprotoName(): + if not extern_key: # if none is spesified - use the name + extern_key = self.getSpec() + + if extern_key: + + self.children.remove(child) + child.parent = None + + extern_child = child.findSpecRecursive(extern_key) + + if extern_child: + self.children.append(extern_child) + extern_child.parent = self + + if DEBUG: + print("\tEXTERNPROTO ID found!:", extern_key) + else: + print("\tEXTERNPROTO ID not found!:", extern_key) + + # Watch it! - restore lines + lines[:] = lines_old + + return new_i + + def __parse(self, i, IS_PROTO_DATA=False): + ''' + print('parsing at', i, end="") + print(i, self.id, self.lineno) + ''' + l = lines[i] + + if l == '[': + # An anonymous list + self.id = None + i += 1 + else: + words = [] + + node_type, new_i = is_nodeline(i, words) + if not node_type: # fail for parsing new node. + print("Failed to parse new node") + raise ValueError + + if self.node_type == NODE_REFERENCE: + # Only assign the reference and quit + key = words[words.index('USE') + 1] + self.id = (words[0],) + + self.reference = self.getDefDict()[key] + return new_i + + self.id = tuple(words) + + # fill in DEF/USE + key = self.getDefName() + if key != None: + self.getDefDict()[key] = self + + key = self.getProtoName() + if not key: + key = self.getExternprotoName() + + proto_dict = self.getProtoDict() + if key != None: + proto_dict[key] = self + + # Parse the proto nodes fields + self.proto_node = vrmlNode(self, NODE_ARRAY, new_i) + new_i = self.proto_node.parse(new_i) + + self.children.remove(self.proto_node) + + # print(self.proto_node) + + new_i += 1 # skip past the { + + else: # If we're a proto instance, add the proto node as our child. + spec = self.getSpec() + try: + self.children.append(proto_dict[spec]) + #pass + except: + pass + + del spec + + del proto_dict, key + + i = new_i + + # print(self.id) + ok = True + while ok: + if i >= len(lines): + return len(lines) - 1 + + l = lines[i] + # print('\tDEBUG:', i, self.node_type, l) + if l == '': + i += 1 + continue + + if l == '}': + if self.node_type != NODE_NORMAL: # also ends proto nodes, we may want a type for these too. + print('wrong node ending, expected an } ' + str(i) + ' ' + str(self.node_type)) + if DEBUG: + raise ValueError + ### print("returning", i) + return i + 1 + if l == ']': + if self.node_type != NODE_ARRAY: + print('wrong node ending, expected a ] ' + str(i) + ' ' + str(self.node_type)) + if DEBUG: + raise ValueError + ### print("returning", i) + return i + 1 + + node_type, new_i = is_nodeline(i, []) + if node_type: # check text\n{ + child = vrmlNode(self, node_type, i) + i = child.parse(i) + + elif l == '[': # some files have these anonymous lists + child = vrmlNode(self, NODE_ARRAY, i) + i = child.parse(i) + + elif is_numline(i): + l_split = l.split(',') + + values = None + # See if each item is a float? + + for num_type in (int, float): + try: + values = [num_type(v) for v in l_split] + break + except: + pass + + try: + values = [[num_type(v) for v in segment.split()] for segment in l_split] + break + except: + pass + + if values == None: # dont parse + values = l_split + + # This should not extend over multiple lines however it is possible + # print(self.array_data) + if values: + self.array_data.extend(values) + i += 1 + else: + words = l.split() + if len(words) > 2 and words[1] == 'USE': + vrmlNode(self, NODE_REFERENCE, i) + else: + + # print("FIELD", i, l) + # + #words = l.split() + ### print('\t\ttag', i) + # this is a tag/ + # print(words, i, l) + value = l + # print(i) + # javastrips can exist as values. + quote_count = l.count('"') + if quote_count % 2: # odd number? + # print('MULTILINE') + while 1: + i += 1 + l = lines[i] + quote_count = l.count('"') + if quote_count % 2: # odd number? + value += '\n' + l[:l.rfind('"')] + break # assume + else: + value += '\n' + l + + value_all = value.split() + + def iskey(k): + if k[0] != '"' and k[0].isalpha() and k.upper() not in ('TRUE', 'FALSE'): + return True + return False + + def split_fields(value): + ''' + key 0.0 otherkey 1,2,3 opt1 opt1 0.0 + -> [key 0.0], [otherkey 1,2,3], [opt1 opt1 0.0] + ''' + field_list = [] + field_context = [] + + for j in range(len(value)): + if iskey(value[j]): + if field_context: + # this IS a key but the previous value was not a key, ot it was a defined field. + if (not iskey(field_context[-1])) or ((len(field_context) == 3 and field_context[1] == 'IS')): + field_list.append(field_context) + + field_context = [value[j]] + else: + # The last item was not a value, multiple keys are needed in some cases. + field_context.append(value[j]) + else: + # Is empty, just add this on + field_context.append(value[j]) + else: + # Add a value to the list + field_context.append(value[j]) + + if field_context: + field_list.append(field_context) + + return field_list + + for value in split_fields(value_all): + # Split + + if value[0] == 'field': + # field SFFloat creaseAngle 4 + self.proto_field_defs.append(value) + else: + self.fields.append(value) + i += 1 + + +def gzipOpen(path): + try: + import gzip + except: + gzip = None + + data = None + if gzip: + try: + data = gzip.open(path, 'r').read() + except: + pass + else: + print('\tNote, gzip module could not be imported, compressed files will fail to load') + + if data == None: + try: + data = open(path, 'rU').read() + except: + pass + + return data + + +def vrml_parse(path): + ''' + Sets up the root node and returns it so load_web3d() can deal with the blender side of things. + Return root (vrmlNode, '') or (None, 'Error String') + ''' + data = gzipOpen(path) + + if data == None: + return None, 'Failed to open file: ' + path + + # Stripped above + lines[:] = vrmlFormat(data) + + lines.insert(0, '{') + lines.insert(0, 'dymmy_node') + lines.append('}') + # Use for testing our parsed output, so we can check on line numbers. + + ''' + ff = open('/tmp/test.txt', 'w') + ff.writelines([l+'\n' for l in lines]) + ff.close() + ''' + + # Now evaluate it + node_type, new_i = is_nodeline(0, []) + if not node_type: + return None, 'Error: VRML file has no starting Node' + + # Trick to make sure we get all root nodes. + lines.insert(0, '{') + lines.insert(0, 'root_node____') # important the name starts with an ascii char + lines.append('}') + + root = vrmlNode(None, NODE_NORMAL, -1) + root.setRoot(path) # we need to set the root so we have a namespace and know the path incase of inlineing + + # Parse recursively + root.parse(0) + + # This prints a load of text + if DEBUG: + print(root) + + return root, '' + + +# ====================== END VRML + +# ====================== X3d Support + +# Sane as vrml but replace the parser +class x3dNode(vrmlNode): + def __init__(self, parent, node_type, x3dNode): + vrmlNode.__init__(self, parent, node_type, -1) + self.x3dNode = x3dNode + + def parse(self, IS_PROTO_DATA=False): + # print(self.x3dNode.tagName) + + define = self.x3dNode.getAttributeNode('DEF') + if define: + self.getDefDict()[define.value] = self + else: + use = self.x3dNode.getAttributeNode('USE') + if use: + try: + self.reference = self.getDefDict()[use.value] + self.node_type = NODE_REFERENCE + except: + print('\tWarning: reference', use.value, 'not found') + self.parent.children.remove(self) + + return + + for x3dChildNode in self.x3dNode.childNodes: + if x3dChildNode.nodeType in (x3dChildNode.TEXT_NODE, x3dChildNode.COMMENT_NODE, x3dChildNode.CDATA_SECTION_NODE): + continue + + node_type = NODE_NORMAL + # print(x3dChildNode, dir(x3dChildNode)) + if x3dChildNode.getAttributeNode('USE'): + node_type = NODE_REFERENCE + + child = x3dNode(self, node_type, x3dChildNode) + child.parse() + + # TODO - x3d Inline + + def getSpec(self): + return self.x3dNode.tagName # should match vrml spec + + def getDefName(self): + data = self.x3dNode.getAttributeNode('DEF') + if data: + data.value # XXX, return?? + return None + + # Other funcs operate from vrml, but this means we can wrap XML fields, still use nice utility funcs + # getFieldAsArray getFieldAsBool etc + def getFieldName(self, field, ancestry, AS_CHILD=False): + # ancestry and AS_CHILD are ignored, only used for VRML now + + self_real = self.getRealNode() # incase we're an instance + field_xml = self.x3dNode.getAttributeNode(field) + if field_xml: + value = field_xml.value + + # We may want to edit. for x3d spesific stuff + # Sucks a bit to return the field name in the list but vrml excepts this :/ + return value.split() + else: + return None + + +def x3d_parse(path): + ''' + Sets up the root node and returns it so load_web3d() can deal with the blender side of things. + Return root (x3dNode, '') or (None, 'Error String') + ''' + + try: + import xml.dom.minidom + except: + return None, 'Error, import XML parsing module (xml.dom.minidom) failed, install python' + + ''' + try: doc = xml.dom.minidom.parse(path) + except: return None, 'Could not parse this X3D file, XML error' + ''' + + # Could add a try/except here, but a console error is more useful. + data = gzipOpen(path) + + if data == None: + return None, 'Failed to open file: ' + path + + doc = xml.dom.minidom.parseString(data) + + try: + x3dnode = doc.getElementsByTagName('X3D')[0] + except: + return None, 'Not a valid x3d document, cannot import' + + root = x3dNode(None, NODE_NORMAL, x3dnode) + root.setRoot(path) # so images and Inline's we load have a relative path + root.parse() + + return root, '' + +## f = open('/_Cylinder.wrl', 'r') +# f = open('/fe/wrl/Vrml/EGS/TOUCHSN.WRL', 'r') +# vrml_parse('/fe/wrl/Vrml/EGS/TOUCHSN.WRL') +#vrml_parse('/fe/wrl/Vrml/EGS/SCRIPT.WRL') +''' +import os +files = os.popen('find /fe/wrl -iname "*.wrl"').readlines() +files.sort() +tot = len(files) +for i, f in enumerate(files): + #if i < 801: + # continue + + f = f.strip() + print(f, i, tot) + vrml_parse(f) +''' + +# NO BLENDER CODE ABOVE THIS LINE. +# ----------------------------------------------------------------------------------- +import bpy +import image_utils +# import BPyImage +# import BPySys +# reload(BPySys) +# reload(BPyImage) +# import Blender +# from Blender import Texture, Material, Mathutils, Mesh, Types, Window +from mathutils import Vector, Matrix + +RAD_TO_DEG = 57.29578 + +GLOBALS = {'CIRCLE_DETAIL': 16} + + +def translateRotation(rot): + ''' axis, angle ''' + return Matrix.Rotation(rot[3], 4, Vector(rot[:3])) + + +def translateScale(sca): + mat = Matrix() # 4x4 default + mat[0][0] = sca[0] + mat[1][1] = sca[1] + mat[2][2] = sca[2] + return mat + + +def translateTransform(node, ancestry): + cent = node.getFieldAsFloatTuple('center', None, ancestry) # (0.0, 0.0, 0.0) + rot = node.getFieldAsFloatTuple('rotation', None, ancestry) # (0.0, 0.0, 1.0, 0.0) + sca = node.getFieldAsFloatTuple('scale', None, ancestry) # (1.0, 1.0, 1.0) + scaori = node.getFieldAsFloatTuple('scaleOrientation', None, ancestry) # (0.0, 0.0, 1.0, 0.0) + tx = node.getFieldAsFloatTuple('translation', None, ancestry) # (0.0, 0.0, 0.0) + + if cent: + cent_mat = Matrix.Translation(Vector(cent)).resize4x4() + cent_imat = cent_mat.copy().invert() + else: + cent_mat = cent_imat = None + + if rot: + rot_mat = translateRotation(rot) + else: + rot_mat = None + + if sca: + sca_mat = translateScale(sca) + else: + sca_mat = None + + if scaori: + scaori_mat = translateRotation(scaori) + scaori_imat = scaori_mat.copy().invert() + else: + scaori_mat = scaori_imat = None + + if tx: + tx_mat = Matrix.Translation(Vector(tx)).resize4x4() + else: + tx_mat = None + + new_mat = Matrix() + + mats = [tx_mat, cent_mat, rot_mat, scaori_mat, sca_mat, scaori_imat, cent_imat] + for mtx in mats: + if mtx: + new_mat = new_mat * mtx + + return new_mat + + +def translateTexTransform(node, ancestry): + cent = node.getFieldAsFloatTuple('center', None, ancestry) # (0.0, 0.0) + rot = node.getFieldAsFloat('rotation', None, ancestry) # 0.0 + sca = node.getFieldAsFloatTuple('scale', None, ancestry) # (1.0, 1.0) + tx = node.getFieldAsFloatTuple('translation', None, ancestry) # (0.0, 0.0) + + if cent: + # cent is at a corner by default + cent_mat = Matrix.Translation(Vector(cent).resize3D()).resize4x4() + cent_imat = cent_mat.copy().invert() + else: + cent_mat = cent_imat = None + + if rot: + rot_mat = Matrix.Rotation(rot, 4, 'Z') # translateRotation(rot) + else: + rot_mat = None + + if sca: + sca_mat = translateScale((sca[0], sca[1], 0.0)) + else: + sca_mat = None + + if tx: + tx_mat = Matrix.Translation(Vector(tx).resize3D()).resize4x4() + else: + tx_mat = None + + new_mat = Matrix() + + # as specified in VRML97 docs + mats = [cent_imat, sca_mat, rot_mat, cent_mat, tx_mat] + + for mtx in mats: + if mtx: + new_mat = new_mat * mtx + + return new_mat + + +# 90d X rotation +import math +MATRIX_Z_TO_Y = Matrix.Rotation(math.pi / 2.0, 4, 'X') + + +def getFinalMatrix(node, mtx, ancestry): + + transform_nodes = [node_tx for node_tx in ancestry if node_tx.getSpec() == 'Transform'] + if node.getSpec() == 'Transform': + transform_nodes.append(node) + transform_nodes.reverse() + + if mtx is None: + mtx = Matrix() + + for node_tx in transform_nodes: + mat = translateTransform(node_tx, ancestry) + mtx = mat * mtx + + # worldspace matrix + mtx = MATRIX_Z_TO_Y * mtx + + return mtx + + +def importMesh_IndexedFaceSet(geom, bpyima, ancestry): + # print(geom.lineno, geom.id, vrmlNode.DEF_NAMESPACE.keys()) + + ccw = geom.getFieldAsBool('ccw', True, ancestry) + ifs_colorPerVertex = geom.getFieldAsBool('colorPerVertex', True, ancestry) # per vertex or per face + ifs_normalPerVertex = geom.getFieldAsBool('normalPerVertex', True, ancestry) + + # This is odd how point is inside Coordinate + + # VRML not x3d + #coord = geom.getChildByName('coord') # 'Coordinate' + + coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml + + if coord: + ifs_points = coord.getFieldAsArray('point', 3, ancestry) + else: + coord = [] + + if not coord: + print('\tWarnint: IndexedFaceSet has no points') + return None, ccw + + ifs_faces = geom.getFieldAsArray('coordIndex', 0, ancestry) + + coords_tex = None + if ifs_faces: # In rare cases this causes problems - no faces but UVs??? + + # WORKS - VRML ONLY + # coords_tex = geom.getChildByName('texCoord') + coords_tex = geom.getChildBySpec('TextureCoordinate') + + if coords_tex: + ifs_texpoints = coords_tex.getFieldAsArray('point', 2, ancestry) + ifs_texfaces = geom.getFieldAsArray('texCoordIndex', 0, ancestry) + + if not ifs_texpoints: + # IF we have no coords, then dont bother + coords_tex = None + + # WORKS - VRML ONLY + # vcolor = geom.getChildByName('color') + vcolor = geom.getChildBySpec('Color') + vcolor_spot = None # spot color when we dont have an array of colors + if vcolor: + # float to char + ifs_vcol = [(0, 0, 0)] # EEKADOODLE - vertex start at 1 + ifs_vcol.extend([col for col in vcolor.getFieldAsArray('color', 3, ancestry)]) + ifs_color_index = geom.getFieldAsArray('colorIndex', 0, ancestry) + + if not ifs_vcol: + vcolor_spot = vcolor.getFieldAsFloatTuple('color', [], ancestry) + + # Convert faces into somthing blender can use + edges = [] + + # All lists are aligned! + faces = [] + faces_uv = [] # if ifs_texfaces is empty then the faces_uv will match faces exactly. + faces_orig_index = [] # for ngons, we need to know our original index + + if coords_tex and ifs_texfaces: + do_uvmap = True + else: + do_uvmap = False + + # current_face = [0] # pointer anyone + + def add_face(face, fuvs, orig_index): + l = len(face) + if l == 3 or l == 4: + faces.append(face) + # faces_orig_index.append(current_face[0]) + if do_uvmap: + faces_uv.append(fuvs) + + faces_orig_index.append(orig_index) + elif l == 2: + edges.append(face) + elif l > 4: + for i in range(2, len(face)): + faces.append([face[0], face[i - 1], face[i]]) + if do_uvmap: + faces_uv.append([fuvs[0], fuvs[i - 1], fuvs[i]]) + faces_orig_index.append(orig_index) + else: + # faces with 1 verts? pfft! + # still will affect index ordering + pass + + face = [] + fuvs = [] + orig_index = 0 + for i, fi in enumerate(ifs_faces): + # ifs_texfaces and ifs_faces should be aligned + if fi != -1: + # face.append(int(fi)) # in rare cases this is a float + # EEKADOODLE!!! + # Annoyance where faces that have a zero index vert get rotated. This will then mess up UVs and VColors + face.append(int(fi) + 1) # in rare cases this is a float, +1 because of stupid EEKADOODLE :/ + + if do_uvmap: + if i >= len(ifs_texfaces): + print('\tWarning: UV Texface index out of range') + fuvs.append(ifs_texfaces[0]) + else: + fuvs.append(ifs_texfaces[i]) + else: + add_face(face, fuvs, orig_index) + face = [] + if do_uvmap: + fuvs = [] + orig_index += 1 + + add_face(face, fuvs, orig_index) + del add_face # dont need this func anymore + + bpymesh = bpy.data.meshes.new(name="XXX") + + # EEKADOODLE + bpymesh.vertices.add(1 + (len(ifs_points))) + bpymesh.vertices.foreach_set("co", [0, 0, 0] + [a for v in ifs_points for a in v]) # XXX25 speed + + # print(len(ifs_points), faces, edges, ngons) + + try: + bpymesh.faces.add(len(faces)) + bpymesh.faces.foreach_set("vertices_raw", [a for f in faces for a in (f + [0] if len(f) == 3 else f)]) # XXX25 speed + except KeyError: + print("one or more vert indicies out of range. corrupt file?") + #for f in faces: + # bpymesh.faces.extend(faces, smooth=True) + + # bpymesh.calcNormals() + bpymesh.update() + + if len(bpymesh.faces) != len(faces): + print('\tWarning: adding faces did not work! file is invalid, not adding UVs or vcolors') + return bpymesh, ccw + + # Apply UVs if we have them + if not do_uvmap: + faces_uv = faces # fallback, we didnt need a uvmap in the first place, fallback to the face/vert mapping. + if coords_tex: + #print(ifs_texpoints) + # print(geom) + uvlay = bpymesh.uv_textures.new() + + for i, f in enumerate(uvlay.data): + f.image = bpyima + fuv = faces_uv[i] # uv indicies + for j, uv in enumerate(f.uv): + # print(fuv, j, len(ifs_texpoints)) + try: + f.uv[j] = ifs_texpoints[fuv[j]] # XXX25, speedup + except: + print('\tWarning: UV Index out of range') + f.uv[j] = ifs_texpoints[0] # XXX25, speedup + + elif bpyima and len(bpymesh.faces): + # Oh Bugger! - we cant really use blenders ORCO for for texture space since texspace dosnt rotate. + # we have to create VRML's coords as UVs instead. + + # VRML docs + ''' + If the texCoord field is NULL, a default texture coordinate mapping is calculated using the local + coordinate system bounding box of the shape. The longest dimension of the bounding box defines the S coordinates, + and the next longest defines the T coordinates. If two or all three dimensions of the bounding box are equal, + ties shall be broken by choosing the X, Y, or Z dimension in that order of preference. + The value of the S coordinate ranges from 0 to 1, from one end of the bounding box to the other. + The T coordinate ranges between 0 and the ratio of the second greatest dimension of the bounding box to the greatest dimension. + ''' + + # Note, S,T == U,V + # U gets longest, V gets second longest + xmin, ymin, zmin = ifs_points[0] + xmax, ymax, zmax = ifs_points[0] + for co in ifs_points: + x, y, z = co + if x < xmin: + xmin = x + if y < ymin: + ymin = y + if z < zmin: + zmin = z + + if x > xmax: + xmax = x + if y > ymax: + ymax = y + if z > zmax: + zmax = z + + xlen = xmax - xmin + ylen = ymax - ymin + zlen = zmax - zmin + + depth_min = xmin, ymin, zmin + depth_list = [xlen, ylen, zlen] + depth_sort = depth_list[:] + depth_sort.sort() + + depth_idx = [depth_list.index(val) for val in depth_sort] + + axis_u = depth_idx[-1] + axis_v = depth_idx[-2] # second longest + + # Hack, swap these !!! TODO - Why swap??? - it seems to work correctly but should not. + # axis_u,axis_v = axis_v,axis_u + + min_u = depth_min[axis_u] + min_v = depth_min[axis_v] + depth_u = depth_list[axis_u] + depth_v = depth_list[axis_v] + + depth_list[axis_u] + + if axis_u == axis_v: + # This should be safe because when 2 axies have the same length, the lower index will be used. + axis_v += 1 + + uvlay = bpymesh.uv_textures.new() + + # HACK !!! - seems to be compatible with Cosmo though. + depth_v = depth_u = max(depth_v, depth_u) + + bpymesh_vertices = bpymesh.vertices[:] + bpymesh_faces = bpymesh.faces[:] + + for j, f in enumerate(uvlay.data): + f.image = bpyima + fuv = f.uv + f_v = bpymesh_faces[j].vertices[:] # XXX25 speed + + for i, v in enumerate(f_v): + co = bpymesh_vertices[v].co + fuv[i] = (co[axis_u] - min_u) / depth_u, (co[axis_v] - min_v) / depth_v + + # Add vcote + if vcolor: + # print(ifs_vcol) + collay = bpymesh.vertex_colors.new() + + for f_idx, f in enumerate(collay.data): + fv = bpymesh.faces[f_idx].vertices[:] + if len(fv) == 3: # XXX speed + fcol = f.color1, f.color2, f.color3 + else: + fcol = f.color1, f.color2, f.color3, f.color4 + if ifs_colorPerVertex: + for i, c in enumerate(fcol): + color_index = fv[i] # color index is vert index + if ifs_color_index: + try: + color_index = ifs_color_index[color_index] + except: + print('\tWarning: per vertex color index out of range') + continue + + if color_index < len(ifs_vcol): + c.r, c.g, c.b = ifs_vcol[color_index] + else: + #print('\tWarning: per face color index out of range') + pass + else: + if vcolor_spot: # use 1 color, when ifs_vcol is [] + for c in fcol: + c.r, c.g, c.b = vcolor_spot + else: + color_index = faces_orig_index[f_idx] # color index is face index + #print(color_index, ifs_color_index) + if ifs_color_index: + if color_index >= len(ifs_color_index): + print('\tWarning: per face color index out of range') + color_index = 0 + else: + color_index = ifs_color_index[color_index] + try: + col = ifs_vcol[color_index] + except IndexError: + # TODO, look + col = (1.0, 1.0, 1.0) + for i, c in enumerate(fcol): + c.r, c.g, c.b = col + + # XXX25 + # bpymesh.vertices.delete([0, ]) # EEKADOODLE + + return bpymesh, ccw + + +def importMesh_IndexedLineSet(geom, ancestry): + # VRML not x3d + #coord = geom.getChildByName('coord') # 'Coordinate' + coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml + if coord: + points = coord.getFieldAsArray('point', 3, ancestry) + else: + points = [] + + if not points: + print('\tWarning: IndexedLineSet had no points') + return None + + ils_lines = geom.getFieldAsArray('coordIndex', 0, ancestry) + + lines = [] + line = [] + + for il in ils_lines: + if il == -1: + lines.append(line) + line = [] + else: + line.append(int(il)) + lines.append(line) + + # vcolor = geom.getChildByName('color') # blender dosnt have per vertex color + + bpycurve = bpy.data.curves.new('IndexedCurve', 'CURVE') + bpycurve.dimensions = '3D' + + for line in lines: + if not line: + continue + co = points[line[0]] + nu = bpycurve.splines.new('POLY') + nu.points.add(len(line)) + + for il, pt in zip(line, nu.points): + pt.co[0:3] = points[il] + + return bpycurve + + +def importMesh_PointSet(geom, ancestry): + # VRML not x3d + #coord = geom.getChildByName('coord') # 'Coordinate' + coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml + if coord: + points = coord.getFieldAsArray('point', 3, ancestry) + else: + points = [] + + # vcolor = geom.getChildByName('color') # blender dosnt have per vertex color + + bpymesh = bpy.data.meshes.new("XXX") + bpymesh.vertices.add(len(points)) + bpymesh.vertices.foreach_set("co", [a for v in points for a in v]) + + # bpymesh.calcNormals() # will just be dummy normals + bpymesh.update() + return bpymesh + +GLOBALS['CIRCLE_DETAIL'] = 12 + + +def bpy_ops_add_object_hack(): # XXX25, evil + scene = bpy.context.scene + obj = scene.objects[0] + scene.objects.unlink(obj) + bpymesh = obj.data + bpy.data.objects.remove(obj) + return bpymesh + + +def importMesh_Sphere(geom, ancestry): + diameter = geom.getFieldAsFloat('radius', 0.5, ancestry) + # bpymesh = Mesh.Primitives.UVsphere(GLOBALS['CIRCLE_DETAIL'], GLOBALS['CIRCLE_DETAIL'], diameter) + + bpy.ops.mesh.primitive_uv_sphere_add(segments=GLOBALS['CIRCLE_DETAIL'], + ring_count=GLOBALS['CIRCLE_DETAIL'], + size=diameter, + view_align=False, + enter_editmode=False, + ) + + bpymesh = bpy_ops_add_object_hack() + + bpymesh.transform(MATRIX_Z_TO_Y) + return bpymesh + + +def importMesh_Cylinder(geom, ancestry): + # bpymesh = bpy.data.meshes.new() + diameter = geom.getFieldAsFloat('radius', 1.0, ancestry) + height = geom.getFieldAsFloat('height', 2, ancestry) + + # bpymesh = Mesh.Primitives.Cylinder(GLOBALS['CIRCLE_DETAIL'], diameter, height) + + bpy.ops.mesh.primitive_cylinder_add(vertices=GLOBALS['CIRCLE_DETAIL'], + radius=diameter, + depth=height, + cap_ends=True, + view_align=False, + enter_editmode=False, + ) + + bpymesh = bpy_ops_add_object_hack() + + bpymesh.transform(MATRIX_Z_TO_Y) + + # Warning - Rely in the order Blender adds verts + # not nice design but wont change soon. + + bottom = geom.getFieldAsBool('bottom', True, ancestry) + side = geom.getFieldAsBool('side', True, ancestry) + top = geom.getFieldAsBool('top', True, ancestry) + + if not top: # last vert is top center of tri fan. + # bpymesh.vertices.delete([(GLOBALS['CIRCLE_DETAIL'] + GLOBALS['CIRCLE_DETAIL']) + 1]) # XXX25 + pass + + if not bottom: # second last vert is bottom of triangle fan + # XXX25 + # bpymesh.vertices.delete([GLOBALS['CIRCLE_DETAIL'] + GLOBALS['CIRCLE_DETAIL']]) + pass + + if not side: + # remove all quads + # XXX25 + # bpymesh.faces.delete(1, [f for f in bpymesh.faces if len(f) == 4]) + pass + + return bpymesh + + +def importMesh_Cone(geom, ancestry): + # bpymesh = bpy.data.meshes.new() + diameter = geom.getFieldAsFloat('bottomRadius', 1.0, ancestry) + height = geom.getFieldAsFloat('height', 2, ancestry) + + # bpymesh = Mesh.Primitives.Cone(GLOBALS['CIRCLE_DETAIL'], diameter, height) + + bpy.ops.mesh.primitive_cone_add(vertices=GLOBALS['CIRCLE_DETAIL'], + radius=diameter, + depth=height, + cap_end=True, + view_align=False, + enter_editmode=False, + ) + + bpymesh = bpy_ops_add_object_hack() + + bpymesh.transform(MATRIX_Z_TO_Y) + + # Warning - Rely in the order Blender adds verts + # not nice design but wont change soon. + + bottom = geom.getFieldAsBool('bottom', True, ancestry) + side = geom.getFieldAsBool('side', True, ancestry) + + if not bottom: # last vert is on the bottom + # bpymesh.vertices.delete([GLOBALS['CIRCLE_DETAIL'] + 1]) # XXX25 + pass + if not side: # second last vert is on the pointy bit of the cone + # bpymesh.vertices.delete([GLOBALS['CIRCLE_DETAIL']]) # XXX25 + pass + + return bpymesh + + +def importMesh_Box(geom, ancestry): + # bpymesh = bpy.data.meshes.new() + + size = geom.getFieldAsFloatTuple('size', (2.0, 2.0, 2.0), ancestry) + + # bpymesh = Mesh.Primitives.Cube(1.0) + bpy.ops.mesh.primitive_cube_add(view_align=False, + enter_editmode=False, + ) + + bpymesh = bpy_ops_add_object_hack() + + # Scale the box to the size set + scale_mat = Matrix(((size[0], 0, 0), (0, size[1], 0), (0, 0, size[2]))) * 0.5 + bpymesh.transform(scale_mat.resize4x4()) + + return bpymesh + + +def importShape(node, ancestry): + vrmlname = node.getDefName() + if not vrmlname: + vrmlname = 'Shape' + + # works 100% in vrml, but not x3d + #appr = node.getChildByName('appearance') # , 'Appearance' + #geom = node.getChildByName('geometry') # , 'IndexedFaceSet' + + # Works in vrml and x3d + appr = node.getChildBySpec('Appearance') + geom = node.getChildBySpec(['IndexedFaceSet', 'IndexedLineSet', 'PointSet', 'Sphere', 'Box', 'Cylinder', 'Cone']) + + # For now only import IndexedFaceSet's + if geom: + bpymat = None + bpyima = None + texmtx = None + + depth = 0 # so we can set alpha face flag later + + if appr: + + #mat = appr.getChildByName('material') # 'Material' + #ima = appr.getChildByName('texture') # , 'ImageTexture' + #if ima and ima.getSpec() != 'ImageTexture': + # print('\tWarning: texture type "%s" is not supported' % ima.getSpec()) + # ima = None + # textx = appr.getChildByName('textureTransform') + + mat = appr.getChildBySpec('Material') + ima = appr.getChildBySpec('ImageTexture') + + textx = appr.getChildBySpec('TextureTransform') + + if textx: + texmtx = translateTexTransform(textx, ancestry) + + # print(mat, ima) + if mat or ima: + + if not mat: + mat = ima # This is a bit dumb, but just means we use default values for all + + # all values between 0.0 and 1.0, defaults from VRML docs + bpymat = bpy.data.materials.new("XXX") + bpymat.ambient = mat.getFieldAsFloat('ambientIntensity', 0.2, ancestry) + bpymat.diffuse_color = mat.getFieldAsFloatTuple('diffuseColor', [0.8, 0.8, 0.8], ancestry) + + # NOTE - blender dosnt support emmisive color + # Store in mirror color and approximate with emit. + emit = mat.getFieldAsFloatTuple('emissiveColor', [0.0, 0.0, 0.0], ancestry) + bpymat.mirror_color = emit + bpymat.emit = (emit[0] + emit[1] + emit[2]) / 3.0 + + bpymat.specular_hardness = int(1 + (510 * mat.getFieldAsFloat('shininess', 0.2, ancestry))) # 0-1 -> 1-511 + bpymat.specular_color = mat.getFieldAsFloatTuple('specularColor', [0.0, 0.0, 0.0], ancestry) + bpymat.alpha = 1.0 - mat.getFieldAsFloat('transparency', 0.0, ancestry) + if bpymat.alpha < 0.999: + bpymat.use_transparency = True + + if ima: + ima_url = ima.getFieldAsString('url', None, ancestry) + + if ima_url == None: + try: + ima_url = ima.getFieldAsStringArray('url', ancestry)[0] # in some cases we get a list of images. + except: + ima_url = None + + if ima_url == None: + print("\twarning, image with no URL, this is odd") + else: + bpyima = image_utils.image_load(ima_url, dirName(node.getFilename()), place_holder=False, recursive=False, convert_callback=imageConvertCompat) + if bpyima: + texture = bpy.data.textures.new("XXX", 'IMAGE') + texture.image = bpyima + + # Adds textures for materials (rendering) + try: + depth = bpyima.depth + except: + depth = -1 + + if depth == 32: + # Image has alpha + bpymat.setTexture(0, texture, Texture.TexCo.UV, Texture.MapTo.COL | Texture.MapTo.ALPHA) + texture.setImageFlags('MipMap', 'InterPol', 'UseAlpha') + bpymat.mode |= Material.Modes.ZTRANSP + bpymat.alpha = 0.0 + else: + mtex = bpymat.texture_slots.add() + mtex.texture = texture + mtex.texture_coords = 'UV' + mtex.use_map_diffuse = True + + ima_repS = ima.getFieldAsBool('repeatS', True, ancestry) + ima_repT = ima.getFieldAsBool('repeatT', True, ancestry) + + # To make this work properly we'd need to scale the UV's too, better to ignore th + # texture.repeat = max(1, ima_repS * 512), max(1, ima_repT * 512) + + if not ima_repS: + bpyima.use_clamp_x = True + if not ima_repT: + bpyima.use_clamp_y = True + + bpydata = None + geom_spec = geom.getSpec() + ccw = True + if geom_spec == 'IndexedFaceSet': + bpydata, ccw = importMesh_IndexedFaceSet(geom, bpyima, ancestry) + elif geom_spec == 'IndexedLineSet': + bpydata = importMesh_IndexedLineSet(geom, ancestry) + elif geom_spec == 'PointSet': + bpydata = importMesh_PointSet(geom, ancestry) + elif geom_spec == 'Sphere': + bpydata = importMesh_Sphere(geom, ancestry) + elif geom_spec == 'Box': + bpydata = importMesh_Box(geom, ancestry) + elif geom_spec == 'Cylinder': + bpydata = importMesh_Cylinder(geom, ancestry) + elif geom_spec == 'Cone': + bpydata = importMesh_Cone(geom, ancestry) + else: + print('\tWarning: unsupported type "%s"' % geom_spec) + return + + if bpydata: + vrmlname = vrmlname + geom_spec + + bpydata.name = vrmlname + + bpyob = node.blendObject = bpy.data.objects.new(vrmlname, bpydata) + bpy.context.scene.objects.link(bpyob) + + if type(bpydata) == bpy.types.Mesh: + is_solid = geom.getFieldAsBool('solid', True, ancestry) + creaseAngle = geom.getFieldAsFloat('creaseAngle', None, ancestry) + + if creaseAngle != None: + bpydata.auto_smooth_angle = 1 + int(min(79, creaseAngle * RAD_TO_DEG)) + bpydata.use_auto_smooth = True + + # Only ever 1 material per shape + if bpymat: + bpydata.materials.append(bpymat) + + if bpydata.uv_textures: + + if depth == 32: # set the faces alpha flag? + transp = Mesh.FaceTranspModes.ALPHA + for f in bpydata.uv_textures.active.data: + f.blend_type = 'ALPHA' + + if texmtx: + # Apply texture transform? + uv_copy = Vector() + for f in bpydata.uv_textures.active.data: + fuv = f.uv + for i, uv in enumerate(fuv): + uv_copy.x = uv[0] + uv_copy.y = uv[1] + + fuv[i] = (uv_copy * texmtx)[0:2] + # Done transforming the texture + + # Must be here and not in IndexedFaceSet because it needs an object for the flip func. Messy :/ + if not ccw: + # bpydata.flipNormals() + # XXX25 + pass + + # else could be a curve for example + + # Can transform data or object, better the object so we can instance the data + #bpymesh.transform(getFinalMatrix(node)) + bpyob.matrix_world = getFinalMatrix(node, None, ancestry) + + +def importLamp_PointLight(node, ancestry): + vrmlname = node.getDefName() + if not vrmlname: + vrmlname = 'PointLight' + + # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0, ancestry) # TODO + # attenuation = node.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO + color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry) + intensity = node.getFieldAsFloat('intensity', 1.0, ancestry) # max is documented to be 1.0 but some files have higher. + location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry) + # is_on = node.getFieldAsBool('on', True, ancestry) # TODO + radius = node.getFieldAsFloat('radius', 100.0, ancestry) + + bpylamp = bpy.data.lamps.new("ToDo", 'POINT') + bpylamp.energy = intensity + bpylamp.distance = radius + bpylamp.color = color + + mtx = Matrix.Translation(Vector(location)) + + return bpylamp, mtx + + +def importLamp_DirectionalLight(node, ancestry): + vrmlname = node.getDefName() + if not vrmlname: + vrmlname = 'DirectLight' + + # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0) # TODO + color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry) + direction = node.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0), ancestry) + intensity = node.getFieldAsFloat('intensity', 1.0, ancestry) # max is documented to be 1.0 but some files have higher. + # is_on = node.getFieldAsBool('on', True, ancestry) # TODO + + bpylamp = bpy.data.lamps.new(vrmlname, 'SUN') + bpylamp.energy = intensity + bpylamp.color = color + + # lamps have their direction as -z, yup + mtx = Vector(direction).to_track_quat('-Z', 'Y').to_matrix().resize4x4() + + return bpylamp, mtx + +# looks like default values for beamWidth and cutOffAngle were swapped in VRML docs. + + +def importLamp_SpotLight(node, ancestry): + vrmlname = node.getDefName() + if not vrmlname: + vrmlname = 'SpotLight' + + # ambientIntensity = geom.getFieldAsFloat('ambientIntensity', 0.0, ancestry) # TODO + # attenuation = geom.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO + beamWidth = node.getFieldAsFloat('beamWidth', 1.570796, ancestry) # max is documented to be 1.0 but some files have higher. + color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry) + cutOffAngle = node.getFieldAsFloat('cutOffAngle', 0.785398, ancestry) * 2.0 # max is documented to be 1.0 but some files have higher. + direction = node.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0), ancestry) + intensity = node.getFieldAsFloat('intensity', 1.0, ancestry) # max is documented to be 1.0 but some files have higher. + location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry) + # is_on = node.getFieldAsBool('on', True, ancestry) # TODO + radius = node.getFieldAsFloat('radius', 100.0, ancestry) + + bpylamp = bpy.data.lamps.new(vrmlname, 'SPOT') + bpylamp.energy = intensity + bpylamp.distance = radius + bpylamp.color = color + bpylamp.spot_size = cutOffAngle + if beamWidth > cutOffAngle: + bpylamp.spot_blend = 0.0 + else: + if cutOffAngle == 0.0: # this should never happen! + bpylamp.spot_blend = 0.5 + else: + bpylamp.spot_blend = beamWidth / cutOffAngle + + # Convert + + # lamps have their direction as -z, y==up + mtx = Matrix.Translation(Vector(location)) * Vector(direction).to_track_quat('-Z', 'Y').to_matrix().resize4x4() + + return bpylamp, mtx + + +def importLamp(node, spec, ancestry): + if spec == 'PointLight': + bpylamp, mtx = importLamp_PointLight(node, ancestry) + elif spec == 'DirectionalLight': + bpylamp, mtx = importLamp_DirectionalLight(node, ancestry) + elif spec == 'SpotLight': + bpylamp, mtx = importLamp_SpotLight(node, ancestry) + else: + print("Error, not a lamp") + raise ValueError + + bpyob = node.blendObject = bpy.data.objects.new("TODO", bpylamp) + bpy.context.scene.objects.link(bpyob) + + bpyob.matrix_world = getFinalMatrix(node, mtx, ancestry) + + +def importViewpoint(node, ancestry): + name = node.getDefName() + if not name: + name = 'Viewpoint' + + fieldOfView = node.getFieldAsFloat('fieldOfView', 0.785398, ancestry) # max is documented to be 1.0 but some files have higher. + # jump = node.getFieldAsBool('jump', True, ancestry) + orientation = node.getFieldAsFloatTuple('orientation', (0.0, 0.0, 1.0, 0.0), ancestry) + position = node.getFieldAsFloatTuple('position', (0.0, 0.0, 0.0), ancestry) + description = node.getFieldAsString('description', '', ancestry) + + bpycam = bpy.data.cameras.new(name) + + bpycam.angle = fieldOfView + + mtx = Matrix.Translation(Vector(position)) * translateRotation(orientation) + + bpyob = node.blendObject = bpy.data.objects.new("TODO", bpycam) + bpy.context.scene.objects.link(bpyob) + bpyob.matrix_world = getFinalMatrix(node, mtx, ancestry) + + +def importTransform(node, ancestry): + name = node.getDefName() + if not name: + name = 'Transform' + + bpyob = node.blendObject = bpy.data.objects.new(name, None) + bpy.context.scene.objects.link(bpyob) + + bpyob.matrix_world = getFinalMatrix(node, None, ancestry) + + # so they are not too annoying + bpyob.empty_draw_type = 'PLAIN_AXES' + bpyob.empty_draw_size = 0.2 + + +#def importTimeSensor(node): +def action_fcurve_ensure(action, data_path, array_index): + for fcu in action.fcurves: + if fcu.data_path == data_path and fcu.array_index == array_index: + return fcu + + return action.fcurves.new(data_path=data_path, array_index=array_index) + + +def translatePositionInterpolator(node, action, ancestry): + key = node.getFieldAsArray('key', 0, ancestry) + keyValue = node.getFieldAsArray('keyValue', 3, ancestry) + + loc_x = action_fcurve_ensure(action, "location", 0) + loc_y = action_fcurve_ensure(action, "location", 1) + loc_z = action_fcurve_ensure(action, "location", 2) + + for i, time in enumerate(key): + try: + x, y, z = keyValue[i] + except: + continue + + loc_x.keyframe_points.add(time, x) + loc_y.keyframe_points.add(time, y) + loc_z.keyframe_points.add(time, z) + + for fcu in (loc_x, loc_y, loc_z): + for kf in fcu.keyframe_points: + kf.interpolation = 'LINEAR' + + +def translateOrientationInterpolator(node, action, ancestry): + key = node.getFieldAsArray('key', 0, ancestry) + keyValue = node.getFieldAsArray('keyValue', 4, ancestry) + + rot_x = action_fcurve_ensure(action, "rotation_euler", 0) + rot_y = action_fcurve_ensure(action, "rotation_euler", 1) + rot_z = action_fcurve_ensure(action, "rotation_euler", 2) + + for i, time in enumerate(key): + try: + x, y, z, w = keyValue[i] + except: + continue + + mtx = translateRotation((x, y, z, w)) + eul = mtx.to_euler() + rot_x.keyframe_points.add(time, eul.x) + rot_y.keyframe_points.add(time, eul.y) + rot_z.keyframe_points.add(time, eul.z) + + for fcu in (rot_x, rot_y, rot_z): + for kf in fcu.keyframe_points: + kf.interpolation = 'LINEAR' + + +# Untested! +def translateScalarInterpolator(node, action, ancestry): + key = node.getFieldAsArray('key', 0, ancestry) + keyValue = node.getFieldAsArray('keyValue', 4, ancestry) + + sca_x = action_fcurve_ensure(action, "scale", 0) + sca_y = action_fcurve_ensure(action, "scale", 1) + sca_z = action_fcurve_ensure(action, "scale", 2) + + for i, time in enumerate(key): + try: + x, y, z = keyValue[i] + except: + continue + + sca_x.keyframe_points.new(time, x) + sca_y.keyframe_points.new(time, y) + sca_z.keyframe_points.new(time, z) + + +def translateTimeSensor(node, action, ancestry): + ''' + Apply a time sensor to an action, VRML has many combinations of loop/start/stop/cycle times + to give different results, for now just do the basics + ''' + + # XXX25 TODO + if 1: + return + + time_cu = action.addCurve('Time') + time_cu.interpolation = Blender.IpoCurve.InterpTypes.LINEAR + + cycleInterval = node.getFieldAsFloat('cycleInterval', None, ancestry) + + startTime = node.getFieldAsFloat('startTime', 0.0, ancestry) + stopTime = node.getFieldAsFloat('stopTime', 250.0, ancestry) + + if cycleInterval != None: + stopTime = startTime + cycleInterval + + loop = node.getFieldAsBool('loop', False, ancestry) + + time_cu.append((1 + startTime, 0.0)) + time_cu.append((1 + stopTime, 1.0 / 10.0)) # anoying, the UI uses /10 + + if loop: + time_cu.extend = Blender.IpoCurve.ExtendTypes.CYCLIC # or - EXTRAP, CYCLIC_EXTRAP, CONST, + + +def importRoute(node, ancestry): + ''' + Animation route only at the moment + ''' + + if not hasattr(node, 'fields'): + return + + routeIpoDict = node.getRouteIpoDict() + + def getIpo(id): + try: + action = routeIpoDict[id] + except: + action = routeIpoDict[id] = bpy.data.actions.new('web3d_ipo') + return action + + # for getting definitions + defDict = node.getDefDict() + ''' + Handles routing nodes to eachother + +ROUTE vpPI.value_changed TO champFly001.set_position +ROUTE vpOI.value_changed TO champFly001.set_orientation +ROUTE vpTs.fraction_changed TO vpPI.set_fraction +ROUTE vpTs.fraction_changed TO vpOI.set_fraction +ROUTE champFly001.bindTime TO vpTs.set_startTime + ''' + + #from_id, from_type = node.id[1].split('.') + #to_id, to_type = node.id[3].split('.') + + #value_changed + set_position_node = None + set_orientation_node = None + time_node = None + + for field in node.fields: + if field and field[0] == 'ROUTE': + try: + from_id, from_type = field[1].split('.') + to_id, to_type = field[3].split('.') + except: + print("Warning, invalid ROUTE", field) + continue + + if from_type == 'value_changed': + if to_type == 'set_position': + action = getIpo(to_id) + set_data_from_node = defDict[from_id] + translatePositionInterpolator(set_data_from_node, action, ancestry) + + if to_type in ('set_orientation', 'rotation'): + action = getIpo(to_id) + set_data_from_node = defDict[from_id] + translateOrientationInterpolator(set_data_from_node, action, ancestry) + + if to_type == 'set_scale': + action = getIpo(to_id) + set_data_from_node = defDict[from_id] + translateScalarInterpolator(set_data_from_node, action, ancestry) + + elif from_type == 'bindTime': + action = getIpo(from_id) + time_node = defDict[to_id] + translateTimeSensor(time_node, action, ancestry) + + +def load_web3d(path, PREF_FLAT=False, PREF_CIRCLE_DIV=16, HELPER_FUNC=None): + + # Used when adding blender primitives + GLOBALS['CIRCLE_DETAIL'] = PREF_CIRCLE_DIV + + #root_node = vrml_parse('/_Cylinder.wrl') + if path.lower().endswith('.x3d'): + root_node, msg = x3d_parse(path) + else: + root_node, msg = vrml_parse(path) + + if not root_node: + print(msg) + return + + # fill with tuples - (node, [parents-parent, parent]) + all_nodes = root_node.getSerialized([], []) + + for node, ancestry in all_nodes: + #if 'castle.wrl' not in node.getFilename(): + # continue + + spec = node.getSpec() + ''' + prefix = node.getPrefix() + if prefix=='PROTO': + pass + else + ''' + if HELPER_FUNC and HELPER_FUNC(node, ancestry): + # Note, include this function so the VRML/X3D importer can be extended + # by an external script. - gets first pick + pass + if spec == 'Shape': + importShape(node, ancestry) + elif spec in ('PointLight', 'DirectionalLight', 'SpotLight'): + importLamp(node, spec, ancestry) + elif spec == 'Viewpoint': + importViewpoint(node, ancestry) + elif spec == 'Transform': + # Only use transform nodes when we are not importing a flat object hierarchy + if PREF_FLAT == False: + importTransform(node, ancestry) + ''' + # These are delt with later within importRoute + elif spec=='PositionInterpolator': + action = bpy.data.ipos.new('web3d_ipo', 'Object') + translatePositionInterpolator(node, action) + ''' + + # After we import all nodes, route events - anim paths + for node, ancestry in all_nodes: + importRoute(node, ancestry) + + for node, ancestry in all_nodes: + if node.isRoot(): + # we know that all nodes referenced from will be in + # routeIpoDict so no need to run node.getDefDict() for every node. + routeIpoDict = node.getRouteIpoDict() + defDict = node.getDefDict() + + for key, action in routeIpoDict.items(): + + # Assign anim curves + node = defDict[key] + if node.blendObject == None: # Add an object if we need one for animation + node.blendObject = bpy.data.objects.new('AnimOb', None) # , name) + bpy.context.scene.objects.link(node.blendObject) + + if node.blendObject.animation_data is None: + node.blendObject.animation_data_create() + + node.blendObject.animation_data.action = action + + # Add in hierarchy + if PREF_FLAT == False: + child_dict = {} + for node, ancestry in all_nodes: + if node.blendObject: + blendObject = None + + # Get the last parent + i = len(ancestry) + while i: + i -= 1 + blendObject = ancestry[i].blendObject + if blendObject: + break + + if blendObject: + # Parent Slow, - 1 liner but works + # blendObject.makeParent([node.blendObject], 0, 1) + + # Parent FAST + try: + child_dict[blendObject].append(node.blendObject) + except: + child_dict[blendObject] = [node.blendObject] + + # Parent + for parent, children in child_dict.items(): + for c in children: + c.parent = parent + + # update deps + bpy.context.scene.update() + del child_dict + + +def load(operator, context, filepath=""): + + load_web3d(filepath, + PREF_FLAT=True, + PREF_CIRCLE_DIV=16, + ) + + return {'FINISHED'} |