- fix [#25246] export default scene to X3D crashes exporter, own fault but also made...
[blender.git] / release / scripts / op / io_scene_x3d / export_x3d.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 # Contributors: bart:neeneenee*de, http://www.neeneenee.de/vrml, Campbell Barton
22
23 """
24 This script exports to X3D format.
25
26 Usage:
27 Run this script from "File->Export" menu.  A pop-up will ask whether you
28 want to export only selected or all relevant objects.
29
30 Known issues:
31     Doesn't handle multiple materials (don't use material indices);<br>
32     Doesn't handle multiple UV textures on a single mesh (create a mesh for each texture);<br>
33     Can't get the texture array associated with material * not the UV ones;
34 """
35
36 import math
37 import os
38
39 import bpy
40 import mathutils
41
42 from io_utils import create_derived_objects, free_derived_objects
43
44 DEG2RAD=0.017453292519943295
45 MATWORLD= mathutils.Matrix.Rotation(-90, 4, 'X')
46
47 def round_color(col, cp):
48     return tuple([round(max(min(c, 1.0), 0.0), cp) for c in col])
49
50 ####################################
51 # Global Variables
52 ####################################
53
54 filepath = ""
55 _safeOverwrite = True
56
57 extension = ''
58
59 ##########################################################
60 # Functions for writing output file
61 ##########################################################
62
63 class x3d_class:
64
65     def __init__(self, filepath):
66         #--- public you can change these ---
67         self.writingcolor = 0
68         self.writingtexture = 0
69         self.writingcoords = 0
70         self.proto = 1
71         self.billnode = 0
72         self.halonode = 0
73         self.collnode = 0
74         self.tilenode = 0
75         self.verbose=2   # level of verbosity in console 0-none, 1-some, 2-most
76         self.cp=3                 # decimals for material color values   0.000 - 1.000
77         self.vp=3                 # decimals for vertex coordinate values  0.000 - n.000
78         self.tp=3                 # decimals for texture coordinate values 0.000 - 1.000
79         self.it=3
80
81         #--- class private don't touch ---
82         self.texNames={}   # dictionary of textureNames
83         self.matNames={}   # dictionary of materiaNames
84         self.meshNames={}   # dictionary of meshNames
85         self.indentLevel=0 # keeps track of current indenting
86         self.filepath=filepath
87         self.file = None
88         if filepath.lower().endswith('.x3dz'):
89             try:
90                 import gzip
91                 self.file = gzip.open(filepath, "w")
92             except:
93                 print("failed to import compression modules, exporting uncompressed")
94                 self.filepath = filepath[:-1] # remove trailing z
95
96         if self.file is None:
97             self.file = open(self.filepath, "w")
98
99         self.bNav=0
100         self.nodeID=0
101         self.namesReserved=[ "Anchor","Appearance","Arc2D","ArcClose2D","AudioClip","Background","Billboard",
102                              "BooleanFilter","BooleanSequencer","BooleanToggle","BooleanTrigger","Box","Circle2D",
103                              "Collision","Color","ColorInterpolator","ColorRGBA","component","Cone","connect",
104                              "Contour2D","ContourPolyline2D","Coordinate","CoordinateDouble","CoordinateInterpolator",
105                              "CoordinateInterpolator2D","Cylinder","CylinderSensor","DirectionalLight","Disk2D",
106                              "ElevationGrid","EspduTransform","EXPORT","ExternProtoDeclare","Extrusion","field",
107                              "fieldValue","FillProperties","Fog","FontStyle","GeoCoordinate","GeoElevationGrid",
108                              "GeoLocationLocation","GeoLOD","GeoMetadata","GeoOrigin","GeoPositionInterpolator",
109                              "GeoTouchSensor","GeoViewpoint","Group","HAnimDisplacer","HAnimHumanoid","HAnimJoint",
110                              "HAnimSegment","HAnimSite","head","ImageTexture","IMPORT","IndexedFaceSet",
111                              "IndexedLineSet","IndexedTriangleFanSet","IndexedTriangleSet","IndexedTriangleStripSet",
112                              "Inline","IntegerSequencer","IntegerTrigger","IS","KeySensor","LineProperties","LineSet",
113                              "LoadSensor","LOD","Material","meta","MetadataDouble","MetadataFloat","MetadataInteger",
114                              "MetadataSet","MetadataString","MovieTexture","MultiTexture","MultiTextureCoordinate",
115                              "MultiTextureTransform","NavigationInfo","Normal","NormalInterpolator","NurbsCurve",
116                              "NurbsCurve2D","NurbsOrientationInterpolator","NurbsPatchSurface",
117                              "NurbsPositionInterpolator","NurbsSet","NurbsSurfaceInterpolator","NurbsSweptSurface",
118                              "NurbsSwungSurface","NurbsTextureCoordinate","NurbsTrimmedSurface","OrientationInterpolator",
119                              "PixelTexture","PlaneSensor","PointLight","PointSet","Polyline2D","Polypoint2D",
120                              "PositionInterpolator","PositionInterpolator2D","ProtoBody","ProtoDeclare","ProtoInstance",
121                              "ProtoInterface","ProximitySensor","ReceiverPdu","Rectangle2D","ROUTE","ScalarInterpolator",
122                              "Scene","Script","Shape","SignalPdu","Sound","Sphere","SphereSensor","SpotLight","StaticGroup",
123                              "StringSensor","Switch","Text","TextureBackground","TextureCoordinate","TextureCoordinateGenerator",
124                              "TextureTransform","TimeSensor","TimeTrigger","TouchSensor","Transform","TransmitterPdu",
125                              "TriangleFanSet","TriangleSet","TriangleSet2D","TriangleStripSet","Viewpoint","VisibilitySensor",
126                              "WorldInfo","X3D","XvlShell","VertexShader","FragmentShader","MultiShaderAppearance","ShaderAppearance" ]
127         self.namesStandard=[ "Empty","Empty.000","Empty.001","Empty.002","Empty.003","Empty.004","Empty.005",
128                              "Empty.006","Empty.007","Empty.008","Empty.009","Empty.010","Empty.011","Empty.012",
129                              "Scene.001","Scene.002","Scene.003","Scene.004","Scene.005","Scene.06","Scene.013",
130                              "Scene.006","Scene.007","Scene.008","Scene.009","Scene.010","Scene.011","Scene.012",
131                              "World","World.000","World.001","World.002","World.003","World.004","World.005" ]
132         self.namesFog=[ "","LINEAR","EXPONENTIAL","" ]
133
134 ##########################################################
135 # Writing nodes routines
136 ##########################################################
137
138     def writeHeader(self):
139         #bfile = sys.expandpath( Blender.Get('filepath') ).replace('<', '&lt').replace('>', '&gt')
140         bfile = repr(os.path.basename(self.filepath).replace('<', '&lt').replace('>', '&gt'))[1:-1] # use outfile name
141         self.file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
142         self.file.write("<!DOCTYPE X3D PUBLIC \"ISO//Web3D//DTD X3D 3.0//EN\" \"http://www.web3d.org/specifications/x3d-3.0.dtd\">\n")
143         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")
144         self.file.write("<head>\n")
145         self.file.write("\t<meta name=\"filename\" content=\"%s\" />\n" % bfile)
146         # self.file.write("\t<meta name=\"filename\" content=\"%s\" />\n" % sys.basename(bfile))
147         self.file.write("\t<meta name=\"generator\" content=\"Blender %s\" />\n" % bpy.app.version_string)
148         # self.file.write("\t<meta name=\"generator\" content=\"Blender %s\" />\n" % Blender.Get('version'))
149         self.file.write("\t<meta name=\"translator\" content=\"X3D exporter v1.55 (2006/01/17)\" />\n")
150         self.file.write("</head>\n")
151         self.file.write("<Scene>\n")
152
153     # This functionality is poorly defined, disabling for now - campbell
154     '''
155     def writeInline(self):
156         inlines = Blender.Scene.Get()
157         allinlines = len(inlines)
158         if scene != inlines[0]:
159             return
160         else:
161             for i in xrange(allinlines):
162                 nameinline=inlines[i].name
163                 if (nameinline not in self.namesStandard) and (i > 0):
164                     self.file.write("<Inline DEF=\"%s\" " % (self.cleanStr(nameinline)))
165                     nameinline = nameinline+".x3d"
166                     self.file.write("url=\"%s\" />" % nameinline)
167                     self.file.write("\n\n")
168
169
170     def writeScript(self):
171         textEditor = Blender.Text.Get()
172         alltext = len(textEditor)
173         for i in xrange(alltext):
174             nametext = textEditor[i].name
175             nlines = textEditor[i].getNLines()
176             if (self.proto == 1):
177                 if (nametext == "proto" or nametext == "proto.js" or nametext == "proto.txt") and (nlines != None):
178                     nalllines = len(textEditor[i].asLines())
179                     alllines = textEditor[i].asLines()
180                     for j in xrange(nalllines):
181                         self.writeIndented(alllines[j] + "\n")
182             elif (self.proto == 0):
183                 if (nametext == "route" or nametext == "route.js" or nametext == "route.txt") and (nlines != None):
184                     nalllines = len(textEditor[i].asLines())
185                     alllines = textEditor[i].asLines()
186                     for j in xrange(nalllines):
187                         self.writeIndented(alllines[j] + "\n")
188         self.writeIndented("\n")
189     '''
190
191     def writeViewpoint(self, ob, mat, scene):
192         context = scene.render
193         # context = scene.render
194         ratio = float(context.resolution_x)/float(context.resolution_y)
195         # ratio = float(context.imageSizeY())/float(context.imageSizeX())
196         lens = (360* (math.atan(ratio *16 / ob.data.lens) / math.pi))*(math.pi/180)
197         # lens = (360* (math.atan(ratio *16 / ob.data.getLens()) / math.pi))*(math.pi/180)
198         lens = min(lens, math.pi)
199
200         # get the camera location, subtract 90 degress from X to orient like X3D does
201         # mat = ob.matrix_world - mat is now passed!
202
203         loc = self.rotatePointForVRML(mat.translation_part())
204         rot = mat.to_euler()
205         rot = (((rot[0]-90)), rot[1], rot[2])
206         # rot = (((rot[0]-90)*DEG2RAD), rot[1]*DEG2RAD, rot[2]*DEG2RAD)
207         nRot = self.rotatePointForVRML( rot )
208         # convert to Quaternion and to Angle Axis
209         Q  = self.eulerToQuaternions(*nRot)
210         Q1 = self.multiplyQuaternions(Q[0], Q[1])
211         Qf = self.multiplyQuaternions(Q1, Q[2])
212         angleAxis = self.quaternionToAngleAxis(Qf)
213         self.file.write("<Viewpoint DEF=\"%s\" " % (self.cleanStr(ob.name)))
214         self.file.write("description=\"%s\" " % (ob.name))
215         self.file.write("centerOfRotation=\"0 0 0\" ")
216         self.file.write("position=\"%3.2f %3.2f %3.2f\" " % loc)
217         self.file.write("orientation=\"%3.2f %3.2f %3.2f %3.2f\" " % (angleAxis[0], angleAxis[1], -angleAxis[2], angleAxis[3]))
218         self.file.write("fieldOfView=\"%.3f\" />\n\n" % (lens))
219
220     def writeFog(self, world):
221         if world:
222             mtype = world.mist_settings.falloff
223             mparam = world.mist_settings
224         else:
225             return
226         if (mtype == 'LINEAR' or mtype == 'INVERSE_QUADRATIC'):
227             mtype = 1 if mtype == 'LINEAR' else 2
228         # if (mtype == 1 or mtype == 2):
229             self.file.write("<Fog fogType=\"%s\" " % self.namesFog[mtype])
230             self.file.write("color=\"%s %s %s\" " % round_color(world.horizon_color, self.cp))
231             self.file.write("visibilityRange=\"%s\" />\n\n" % round(mparam[2],self.cp))
232         else:
233             return
234
235     def writeNavigationInfo(self, scene):
236         self.file.write('<NavigationInfo headlight="false" visibilityLimit="0.0" type=\'"EXAMINE","ANY"\' avatarSize="0.25, 1.75, 0.75" />\n')
237
238     def writeSpotLight(self, ob, mtx, lamp, world):
239         safeName = self.cleanStr(ob.name)
240         if world:
241             ambi = world.ambient_color
242             ambientIntensity = ((ambi[0] + ambi[1] + ambi[2]) / 3.0) / 2.5
243             del ambi
244         else:
245             ambientIntensity = 0.0
246
247         # compute cutoff and beamwidth
248         intensity=min(lamp.energy/1.75,1.0)
249         beamWidth=lamp.spot_size * 0.37;
250         # beamWidth=((lamp.spotSize*math.pi)/180.0)*.37;
251         cutOffAngle=beamWidth*1.3
252
253         dx,dy,dz=self.computeDirection(mtx)
254         # note -dx seems to equal om[3][0]
255         # note -dz seems to equal om[3][1]
256         # note  dy seems to equal om[3][2]
257
258         #location=(ob.matrix_world*MATWORLD).translation_part() # now passed
259         location=(MATWORLD * mtx).translation_part()
260
261         radius = lamp.distance*math.cos(beamWidth)
262         # radius = lamp.dist*math.cos(beamWidth)
263         self.file.write("<SpotLight DEF=\"%s\" " % safeName)
264         self.file.write("radius=\"%s\" " % (round(radius,self.cp)))
265         self.file.write("ambientIntensity=\"%s\" " % (round(ambientIntensity,self.cp)))
266         self.file.write("intensity=\"%s\" " % (round(intensity,self.cp)))
267         self.file.write("color=\"%s %s %s\" " % round_color(lamp.color, self.cp))
268         self.file.write("beamWidth=\"%s\" " % (round(beamWidth,self.cp)))
269         self.file.write("cutOffAngle=\"%s\" " % (round(cutOffAngle,self.cp)))
270         self.file.write("direction=\"%s %s %s\" " % (round(dx,3),round(dy,3),round(dz,3)))
271         self.file.write("location=\"%s %s %s\" />\n\n" % (round(location[0],3), round(location[1],3), round(location[2],3)))
272
273
274     def writeDirectionalLight(self, ob, mtx, lamp, world):
275         safeName = self.cleanStr(ob.name)
276         if world:
277             ambi = world.ambient_color
278             # ambi = world.amb
279             ambientIntensity = ((float(ambi[0] + ambi[1] + ambi[2]))/3)/2.5
280         else:
281             ambi = 0
282             ambientIntensity = 0
283
284         intensity=min(lamp.energy/1.75,1.0)
285         (dx,dy,dz)=self.computeDirection(mtx)
286         self.file.write("<DirectionalLight DEF=\"%s\" " % safeName)
287         self.file.write("ambientIntensity=\"%s\" " % (round(ambientIntensity,self.cp)))
288         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)))
289         # self.file.write("color=\"%s %s %s\" " % (round(lamp.col[0],self.cp), round(lamp.col[1],self.cp), round(lamp.col[2],self.cp)))
290         self.file.write("intensity=\"%s\" " % (round(intensity,self.cp)))
291         self.file.write("direction=\"%s %s %s\" />\n\n" % (round(dx,4),round(dy,4),round(dz,4)))
292
293     def writePointLight(self, ob, mtx, lamp, world):
294         safeName = self.cleanStr(ob.name)
295         if world:
296             ambi = world.ambient_color
297             # ambi = world.amb
298             ambientIntensity = ((float(ambi[0] + ambi[1] + ambi[2]))/3)/2.5
299         else:
300             ambi = 0
301             ambientIntensity = 0
302
303         location= (MATWORLD * mtx).translation_part()
304
305         self.file.write("<PointLight DEF=\"%s\" " % safeName)
306         self.file.write("ambientIntensity=\"%s\" " % (round(ambientIntensity,self.cp)))
307         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)))
308         # self.file.write("color=\"%s %s %s\" " % (round(lamp.col[0],self.cp), round(lamp.col[1],self.cp), round(lamp.col[2],self.cp)))
309         self.file.write("intensity=\"%s\" " % (round( min(lamp.energy/1.75,1.0) ,self.cp)))
310         self.file.write("radius=\"%s\" " % lamp.distance )
311         # self.file.write("radius=\"%s\" " % lamp.dist )
312         self.file.write("location=\"%s %s %s\" />\n\n" % (round(location[0],3), round(location[1],3), round(location[2],3)))
313     '''
314     def writeNode(self, ob, mtx):
315         obname=str(ob.name)
316         if obname in self.namesStandard:
317             return
318         else:
319             dx,dy,dz = self.computeDirection(mtx)
320             # location=(MATWORLD * ob.matrix_world).translation_part()
321             location=(MATWORLD * mtx).translation_part()
322             self.writeIndented("<%s\n" % obname,1)
323             self.writeIndented("direction=\"%s %s %s\"\n" % (round(dx,3),round(dy,3),round(dz,3)))
324             self.writeIndented("location=\"%s %s %s\"\n" % (round(location[0],3), round(location[1],3), round(location[2],3)))
325             self.writeIndented("/>\n",-1)
326             self.writeIndented("\n")
327     '''
328     def secureName(self, name):
329         name = name + str(self.nodeID)
330         self.nodeID=self.nodeID+1
331         if len(name) <= 3:
332             newname = "_" + str(self.nodeID)
333             return "%s" % (newname)
334         else:
335             for bad in ['"','#',"'",',','.','[','\\',']','{','}']:
336                 name=name.replace(bad,'_')
337             if name in self.namesReserved:
338                 newname = name[0:3] + "_" + str(self.nodeID)
339                 return "%s" % (newname)
340             elif name[0].isdigit():
341                 newname = "_" + name + str(self.nodeID)
342                 return "%s" % (newname)
343             else:
344                 newname = name
345                 return "%s" % (newname)
346
347     def writeIndexedFaceSet(self, ob, mesh, mtx, world, EXPORT_TRI = False):
348         imageMap={}   # set of used images
349         sided={}          # 'one':cnt , 'two':cnt
350         meshName = self.cleanStr(ob.name)
351
352         meshME = self.cleanStr(ob.data.name) # We dont care if its the mesh name or not
353         # meshME = self.cleanStr(ob.getData(mesh=1).name) # We dont care if its the mesh name or not
354         if len(mesh.faces) == 0: return
355         mode = []
356         # mode = 0
357         if mesh.uv_textures.active:
358         # if mesh.faceUV:
359             for face in mesh.uv_textures.active.data:
360             # for face in mesh.faces:
361                 if face.use_halo and 'HALO' not in mode:
362                     mode += ['HALO']
363                 if face.use_billboard and 'BILLBOARD' not in mode:
364                     mode += ['BILLBOARD']
365                 if face.use_object_color and 'OBJECT_COLOR' not in mode:
366                     mode += ['OBJECT_COLOR']
367                 if face.use_collision and 'COLLISION' not in mode:
368                     mode += ['COLLISION']
369                 # mode |= face.mode
370
371         if 'HALO' in mode and self.halonode == 0:
372         # if mode & Mesh.FaceModes.HALO and self.halonode == 0:
373             self.writeIndented("<Billboard axisOfRotation=\"0 0 0\">\n",1)
374             self.halonode = 1
375         elif 'BILLBOARD' in mode and self.billnode == 0:
376         # elif mode & Mesh.FaceModes.BILLBOARD and self.billnode == 0:
377             self.writeIndented("<Billboard axisOfRotation=\"0 1 0\">\n",1)
378             self.billnode = 1
379         # TF_TILES is marked as deprecated in DNA_meshdata_types.h
380         # elif mode & Mesh.FaceModes.TILES and self.tilenode == 0:
381         #       self.tilenode = 1
382         elif 'COLLISION' not in mode and self.collnode == 0:
383         # elif not mode & Mesh.FaceModes.DYNAMIC and self.collnode == 0:
384             self.writeIndented("<Collision enabled=\"false\">\n",1)
385             self.collnode = 1
386
387         nIFSCnt=self.countIFSSetsNeeded(mesh, imageMap, sided)
388
389         if nIFSCnt > 1:
390             self.writeIndented("<Group DEF=\"%s%s\">\n" % ("G_", meshName),1)
391
392         if 'two' in sided and sided['two'] > 0:
393             bTwoSided=1
394         else:
395             bTwoSided=0
396
397         mtx = MATWORLD * mtx
398
399         loc= mtx.translation_part()
400         sca= mtx.scale_part()
401         quat = mtx.to_quat()
402         rot= quat.axis
403
404         self.writeIndented('<Transform DEF="%s" translation="%.6f %.6f %.6f" scale="%.6f %.6f %.6f" rotation="%.6f %.6f %.6f %.6f">\n' % \
405                            (meshName, loc[0], loc[1], loc[2], sca[0], sca[1], sca[2], rot[0], rot[1], rot[2], quat.angle) )
406         # self.writeIndented('<Transform DEF="%s" translation="%.6f %.6f %.6f" scale="%.6f %.6f %.6f" rotation="%.6f %.6f %.6f %.6f">\n' % \
407         #   (meshName, loc[0], loc[1], loc[2], sca[0], sca[1], sca[2], rot[0], rot[1], rot[2], quat.angle*DEG2RAD) )
408
409         self.writeIndented("<Shape>\n",1)
410         maters=mesh.materials
411         hasImageTexture = False
412         is_smooth = False
413
414         if len(maters) > 0 or mesh.uv_textures.active:
415         # if len(maters) > 0 or mesh.faceUV:
416             self.writeIndented("<Appearance>\n", 1)
417             # right now this script can only handle a single material per mesh.
418             if len(maters) >= 1 and maters[0].use_face_texture == False:
419                 mat = maters[0]
420                 self.writeMaterial(mat, self.cleanStr(mat.name,''), world)
421                 if len(maters) > 1:
422                     print("Warning: mesh named %s has multiple materials" % meshName)
423                     print("Warning: only one material per object handled")
424
425             if not len(maters) or maters[0].use_face_texture:
426                 #-- textures
427                 image = None
428                 if mesh.uv_textures.active:
429                     for face in mesh.uv_textures.active.data:
430                         if face.use_image:
431                             image = face.image
432                             if image:
433                                 self.writeImageTexture(image)
434                                 break
435
436                 if image:
437                     hasImageTexture = True
438
439                     if self.tilenode == 1:
440                         self.writeIndented("<TextureTransform   scale=\"%s %s\" />\n" % (image.xrep, image.yrep))
441                         self.tilenode = 0
442
443             self.writeIndented("</Appearance>\n", -1)
444
445         #-- IndexedFaceSet or IndexedLineSet
446
447         # user selected BOUNDS=1, SOLID=3, SHARED=4, or TEXTURE=5
448         ifStyle="IndexedFaceSet"
449         # look up mesh name, use it if available
450         if meshME in self.meshNames:
451             self.writeIndented("<%s USE=\"ME_%s\">" % (ifStyle, meshME), 1)
452             self.meshNames[meshME]+=1
453         else:
454             if int(mesh.users) > 1:
455                 self.writeIndented("<%s DEF=\"ME_%s\" " % (ifStyle, meshME), 1)
456                 self.meshNames[meshME]=1
457             else:
458                 self.writeIndented("<%s " % ifStyle, 1)
459
460             if bTwoSided == 1:
461                 self.file.write("solid=\"false\" ")
462             else:
463                 self.file.write("solid=\"true\" ")
464
465             for face in mesh.faces:
466                 if face.use_smooth:
467                     is_smooth = True
468                     break
469             if is_smooth == True:
470                 creaseAngle=(mesh.auto_smooth_angle)*(math.pi/180.0)
471                 # creaseAngle=(mesh.degr)*(math.pi/180.0)
472                 self.file.write("creaseAngle=\"%s\" " % (round(creaseAngle,self.cp)))
473
474             #--- output textureCoordinates if UV texture used
475             if mesh.uv_textures.active:
476                 self.writeTextureCoordinates(mesh)
477             if mesh.vertex_colors.active:
478                 self.writeFaceColors(mesh)
479             #--- output coordinates
480             self.writeCoordinates(ob, mesh, meshName, EXPORT_TRI)
481
482             self.writingcoords = 1
483             self.writingtexture = 1
484             self.writingcolor = 1
485             self.writeCoordinates(ob, mesh, meshName, EXPORT_TRI)
486
487             #--- output textureCoordinates if UV texture used
488             if mesh.uv_textures.active:
489                 self.writeTextureCoordinates(mesh)
490             if mesh.vertex_colors.active:
491                 self.writeFaceColors(mesh)
492             #--- output vertexColors
493
494         self.writingcoords = 0
495         self.writingtexture = 0
496         self.writingcolor = 0
497         #--- output closing braces
498         self.writeIndented("</%s>\n" % ifStyle, -1)
499         self.writeIndented("</Shape>\n", -1)
500         self.writeIndented("</Transform>\n", -1)
501
502         if self.halonode == 1:
503             self.writeIndented("</Billboard>\n", -1)
504             self.halonode = 0
505
506         if self.billnode == 1:
507             self.writeIndented("</Billboard>\n", -1)
508             self.billnode = 0
509
510         if self.collnode == 1:
511             self.writeIndented("</Collision>\n", -1)
512             self.collnode = 0
513
514         if nIFSCnt > 1:
515             self.writeIndented("</Group>\n", -1)
516
517         self.file.write("\n")
518
519     def writeCoordinates(self, ob, mesh, meshName, EXPORT_TRI = False):
520         # create vertex list and pre rotate -90 degrees X for VRML
521
522         if self.writingcoords == 0:
523             self.file.write('coordIndex="')
524             for face in mesh.faces:
525                 fv = face.vertices
526                 # fv = face.v
527
528                 if len(fv)==3:
529                 # if len(face)==3:
530                     self.file.write("%i %i %i -1, " % (fv[0], fv[1], fv[2]))
531                     # self.file.write("%i %i %i -1, " % (fv[0].index, fv[1].index, fv[2].index))
532                 else:
533                     if EXPORT_TRI:
534                         self.file.write("%i %i %i -1, " % (fv[0], fv[1], fv[2]))
535                         # self.file.write("%i %i %i -1, " % (fv[0].index, fv[1].index, fv[2].index))
536                         self.file.write("%i %i %i -1, " % (fv[0], fv[2], fv[3]))
537                         # self.file.write("%i %i %i -1, " % (fv[0].index, fv[2].index, fv[3].index))
538                     else:
539                         self.file.write("%i %i %i %i -1, " % (fv[0], fv[1], fv[2], fv[3]))
540                         # self.file.write("%i %i %i %i -1, " % (fv[0].index, fv[1].index, fv[2].index, fv[3].index))
541
542             self.file.write("\">\n")
543         else:
544             #-- vertices
545             # mesh.transform(ob.matrix_world)
546             self.writeIndented("<Coordinate DEF=\"%s%s\" \n" % ("coord_",meshName), 1)
547             self.file.write("\t\t\t\tpoint=\"")
548             for v in mesh.vertices:
549                 self.file.write("%.6f %.6f %.6f, " % tuple(v.co))
550             self.file.write("\" />")
551             self.writeIndented("\n", -1)
552
553     def writeTextureCoordinates(self, mesh):
554         texCoordList=[]
555         texIndexList=[]
556         j=0
557
558         for face in mesh.uv_textures.active.data:
559         # for face in mesh.faces:
560             # workaround, since tface.uv iteration is wrong atm
561             uvs = face.uv
562             # uvs = [face.uv1, face.uv2, face.uv3, face.uv4] if face.vertices[3] else [face.uv1, face.uv2, face.uv3]
563
564             for uv in uvs:
565             # for uv in face.uv:
566                 texIndexList.append(j)
567                 texCoordList.append(uv)
568                 j=j+1
569             texIndexList.append(-1)
570
571         if self.writingtexture == 0:
572             self.file.write("\n\t\t\ttexCoordIndex=\"")
573             texIndxStr=""
574             for i in range(len(texIndexList)):
575                 texIndxStr = texIndxStr + "%d, " % texIndexList[i]
576                 if texIndexList[i]==-1:
577                     self.file.write(texIndxStr)
578                     texIndxStr=""
579             self.file.write("\"\n\t\t\t")
580         else:
581             self.writeIndented("<TextureCoordinate point=\"", 1)
582             for i in range(len(texCoordList)):
583                 self.file.write("%s %s, " % (round(texCoordList[i][0],self.tp), round(texCoordList[i][1],self.tp)))
584             self.file.write("\" />")
585             self.writeIndented("\n", -1)
586
587     def writeFaceColors(self, mesh):
588         if self.writingcolor == 0:
589             self.file.write("colorPerVertex=\"false\" ")
590         elif mesh.vertex_colors.active:
591         # else:
592             self.writeIndented("<Color color=\"", 1)
593             for face in mesh.vertex_colors.active.data:
594                 c = face.color1
595                 if self.verbose > 2:
596                     print("Debug: face.col r=%d g=%d b=%d" % (c[0], c[1], c[2]))
597                     # print("Debug: face.col r=%d g=%d b=%d" % (c.r, c.g, c.b))
598                 aColor = self.rgbToFS(c)
599                 self.file.write("%s, " % aColor)
600
601             # for face in mesh.faces:
602             #   if face.col:
603             #           c=face.col[0]
604             #           if self.verbose > 2:
605             #                   print("Debug: face.col r=%d g=%d b=%d" % (c.r, c.g, c.b))
606             #           aColor = self.rgbToFS(c)
607             #           self.file.write("%s, " % aColor)
608             self.file.write("\" />")
609             self.writeIndented("\n",-1)
610
611     def writeMaterial(self, mat, matName, world):
612         # look up material name, use it if available
613         if matName in self.matNames:
614             self.writeIndented("<Material USE=\"MA_%s\" />\n" % matName)
615             self.matNames[matName]+=1
616             return;
617
618         self.matNames[matName] = 1
619
620         emit = mat.emit
621         ambient = mat.ambient / 3.0
622         diffuseColor = tuple(mat.diffuse_color)
623         if world:
624             ambiColor = tuple(((c * mat.ambient) * 2.0) for c in world.ambient_color)
625         else:
626             ambiColor = 0.0, 0.0, 0.0
627
628         emitColor = tuple(((c * emit) + ambiColor[i]) / 2.0 for i, c in enumerate(diffuseColor))
629         shininess = mat.specular_hardness / 512.0
630         specColor = tuple((c + 0.001) / (1.25 / (mat.specular_intensity + 0.001)) for c in mat.specular_color)
631         transp = 1.0 - mat.alpha
632
633         if mat.use_shadeless:
634             ambient = 1.0
635             shininess = 0.0
636             specColor = emitColor = diffuseColor
637
638         self.writeIndented("<Material DEF=\"MA_%s\" " % matName, 1)
639         self.file.write("diffuseColor=\"%s %s %s\" " % round_color(diffuseColor, self.cp))
640         self.file.write("specularColor=\"%s %s %s\" " % round_color(specColor, self.cp))
641         self.file.write("emissiveColor=\"%s %s %s\" \n" % round_color(emitColor, self.cp))
642         self.writeIndented("ambientIntensity=\"%s\" " % (round(ambient, self.cp)))
643         self.file.write("shininess=\"%s\" " % (round(shininess, self.cp)))
644         self.file.write("transparency=\"%s\" />" % (round(transp, self.cp)))
645         self.writeIndented("\n",-1)
646
647     def writeImageTexture(self, image):
648         name = image.name
649         filepath = os.path.basename(image.filepath)
650         if name in self.texNames:
651             self.writeIndented("<ImageTexture USE=\"%s\" />\n" % self.cleanStr(name))
652             self.texNames[name] += 1
653         else:
654             self.writeIndented("<ImageTexture DEF=\"%s\" " % self.cleanStr(name), 1)
655             self.file.write("url=\"%s\" />" % filepath)
656             self.writeIndented("\n",-1)
657             self.texNames[name] = 1
658
659     def writeBackground(self, world, alltextures):
660         if world:       worldname = world.name
661         else:           return
662         blending = world.use_sky_blend, world.use_sky_paper, world.use_sky_real
663
664         grd_triple = round_color(world.horizon_color, self.cp)
665         sky_triple = round_color(world.zenith_color, self.cp)
666         mix_triple = round_color(((grd_triple[i] + sky_triple[i]) / 2.0 for i in range(3)), self.cp)
667
668         self.file.write("<Background ")
669         if worldname not in self.namesStandard:
670             self.file.write("DEF=\"%s\" " % self.secureName(worldname))
671         # No Skytype - just Hor color
672         if blending == (False, False, False):
673             self.file.write("groundColor=\"%s %s %s\" " % grd_triple)
674             self.file.write("skyColor=\"%s %s %s\" " % grd_triple)
675         # Blend Gradient
676         elif blending == (True, False, False):
677             self.file.write("groundColor=\"%s %s %s, " % grd_triple)
678             self.file.write("%s %s %s\" groundAngle=\"1.57, 1.57\" " % mix_triple)
679             self.file.write("skyColor=\"%s %s %s, " % sky_triple)
680             self.file.write("%s %s %s\" skyAngle=\"1.57, 1.57\" " % mix_triple)
681         # Blend+Real Gradient Inverse
682         elif blending == (True, False, True):
683             self.file.write("groundColor=\"%s %s %s, " % sky_triple)
684             self.file.write("%s %s %s\" groundAngle=\"1.57, 1.57\" " % mix_triple)
685             self.file.write("skyColor=\"%s %s %s, " % grd_triple)
686             self.file.write("%s %s %s\" skyAngle=\"1.57, 1.57\" " % mix_triple)
687         # Paper - just Zen Color
688         elif blending == (False, False, True):
689             self.file.write("groundColor=\"%s %s %s\" " % sky_triple)
690             self.file.write("skyColor=\"%s %s %s\" " % sky_triple)
691         # Blend+Real+Paper - komplex gradient
692         elif blending == (True, True, True):
693             self.writeIndented("groundColor=\"%s %s %s, " % sky_triple)
694             self.writeIndented("%s %s %s\" groundAngle=\"1.57, 1.57\" " % grd_triple)
695             self.writeIndented("skyColor=\"%s %s %s, " % sky_triple)
696             self.writeIndented("%s %s %s\" skyAngle=\"1.57, 1.57\" " % grd_triple)
697         # Any Other two colors
698         else:
699             self.file.write("groundColor=\"%s %s %s\" " % grd_triple)
700             self.file.write("skyColor=\"%s %s %s\" " % sky_triple)
701
702         alltexture = len(alltextures)
703
704         for i in range(alltexture):
705             tex = alltextures[i]
706
707             if tex.type != 'IMAGE' or tex.image is None:
708                 continue
709
710             namemat = tex.name
711             # namemat = alltextures[i].name
712
713             pic = tex.image
714
715             # using .expandpath just in case, os.path may not expect //
716             basename = os.path.basename(bpy.path.abspath(pic.filepath))
717
718             pic = alltextures[i].image
719             if (namemat == "back") and (pic != None):
720                 self.file.write("\n\tbackUrl=\"%s\" " % basename)
721             elif (namemat == "bottom") and (pic != None):
722                 self.writeIndented("bottomUrl=\"%s\" " % basename)
723             elif (namemat == "front") and (pic != None):
724                 self.writeIndented("frontUrl=\"%s\" " % basename)
725             elif (namemat == "left") and (pic != None):
726                 self.writeIndented("leftUrl=\"%s\" " % basename)
727             elif (namemat == "right") and (pic != None):
728                 self.writeIndented("rightUrl=\"%s\" " % basename)
729             elif (namemat == "top") and (pic != None):
730                 self.writeIndented("topUrl=\"%s\" " % basename)
731         self.writeIndented("/>\n\n")
732
733 ##########################################################
734 # export routine
735 ##########################################################
736
737     def export(self, scene, world, alltextures,\
738             EXPORT_APPLY_MODIFIERS = False,\
739             EXPORT_TRI=                         False,\
740         ):
741
742         print("Info: starting X3D export to %r..." % self.filepath)
743         self.writeHeader()
744         # self.writeScript()
745         self.writeNavigationInfo(scene)
746         self.writeBackground(world, alltextures)
747         self.writeFog(world)
748         self.proto = 0
749
750
751         # # COPIED FROM OBJ EXPORTER
752         # if EXPORT_APPLY_MODIFIERS:
753         #       temp_mesh_name = '~tmp-mesh'
754
755         #       # Get the container mesh. - used for applying modifiers and non mesh objects.
756         #       containerMesh = meshName = tempMesh = None
757         #       for meshName in Blender.NMesh.GetNames():
758         #               if meshName.startswith(temp_mesh_name):
759         #                       tempMesh = Mesh.Get(meshName)
760         #                       if not tempMesh.users:
761         #                               containerMesh = tempMesh
762         #       if not containerMesh:
763         #               containerMesh = Mesh.New(temp_mesh_name)
764         # --------------------------
765
766
767         for ob_main in [o for o in scene.objects if o.is_visible(scene)]:
768         # for ob_main in scene.objects.context:
769
770             free, derived = create_derived_objects(scene, ob_main)
771
772             if derived is None: continue
773
774             for ob, ob_mat in derived:
775             # for ob, ob_mat in BPyObject.getDerivedObjects(ob_main):
776                 objType=ob.type
777                 objName=ob.name
778                 if objType == 'CAMERA':
779                     self.writeViewpoint(ob, ob_mat, scene)
780                 elif objType in ('MESH', 'CURVE', 'SURF', 'FONT') :
781                     if EXPORT_APPLY_MODIFIERS or objType != 'MESH':
782                         me = ob.create_mesh(scene, EXPORT_APPLY_MODIFIERS, 'PREVIEW')
783                     else:
784                         me = ob.data
785
786                     self.writeIndexedFaceSet(ob, me, ob_mat, world, EXPORT_TRI = EXPORT_TRI)
787
788                     # free mesh created with create_mesh()
789                     if me != ob.data:
790                         bpy.data.meshes.remove(me)
791
792                 elif objType == 'LAMP':
793                     data= ob.data
794                     datatype=data.type
795                     if datatype == 'POINT':
796                         self.writePointLight(ob, ob_mat, data, world)
797                     elif datatype == 'SPOT':
798                         self.writeSpotLight(ob, ob_mat, data, world)
799                     elif datatype == 'SUN':
800                         self.writeDirectionalLight(ob, ob_mat, data, world)
801                     else:
802                         self.writeDirectionalLight(ob, ob_mat, data, world)
803                 else:
804                     #print "Info: Ignoring [%s], object type [%s] not handle yet" % (object.name,object.getType)
805                     pass
806
807             if free:
808                 free_derived_objects(ob_main)
809
810         self.file.write("\n</Scene>\n</X3D>")
811
812         # if EXPORT_APPLY_MODIFIERS:
813         #       if containerMesh:
814         #               containerMesh.vertices = None
815
816         self.cleanup()
817
818 ##########################################################
819 # Utility methods
820 ##########################################################
821
822     def cleanup(self):
823         self.file.close()
824         self.texNames={}
825         self.matNames={}
826         self.indentLevel=0
827         print("Info: finished X3D export to %r" % self.filepath)
828
829     def cleanStr(self, name, prefix='rsvd_'):
830         """cleanStr(name,prefix) - try to create a valid VRML DEF name from object name"""
831
832         newName=name[:]
833         if len(newName) == 0:
834             self.nNodeID+=1
835             return "%s%d" % (prefix, self.nNodeID)
836
837         if newName in self.namesReserved:
838             newName='%s%s' % (prefix,newName)
839
840         if newName[0].isdigit():
841             newName='%s%s' % ('_',newName)
842
843         for bad in [' ','"','#',"'",',','.','[','\\',']','{','}']:
844             newName=newName.replace(bad,'_')
845         return newName
846
847     def countIFSSetsNeeded(self, mesh, imageMap, sided):
848         """
849         countIFFSetsNeeded() - should look at a blender mesh to determine
850         how many VRML IndexFaceSets or IndexLineSets are needed.  A
851         new mesh created under the following conditions:
852
853          o - split by UV Textures / one per mesh
854          o - split by face, one sided and two sided
855          o - split by smooth and flat faces
856          o - split when faces only have 2 vertices * needs to be an IndexLineSet
857         """
858
859         imageNameMap={}
860         faceMap={}
861         nFaceIndx=0
862
863         if mesh.uv_textures.active:
864         # if mesh.faceUV:
865             for face in mesh.uv_textures.active.data:
866             # for face in mesh.faces
867                 sidename = "two" if face.use_twoside else "one"
868
869                 if sidename in sided:
870                     sided[sidename]+=1
871                 else:
872                     sided[sidename]=1
873
874                 image = face.image
875                 if image:
876                     faceName="%s_%s" % (face.image.name, sidename);
877                     try:
878                         imageMap[faceName].append(face)
879                     except:
880                         imageMap[faceName]=[face.image.name,sidename,face]
881
882             if self.verbose > 2:
883                 for faceName in imageMap.keys():
884                     ifs=imageMap[faceName]
885                     print("Debug: faceName=%s image=%s, solid=%s facecnt=%d" % \
886                           (faceName, ifs[0], ifs[1], len(ifs)-2))
887
888         return len(imageMap)
889
890     def faceToString(self,face):
891
892         print("Debug: face.flag=0x%x (bitflags)" % face.flag)
893         if face.sel:
894             print("Debug: face.sel=true")
895
896         print("Debug: face.mode=0x%x (bitflags)" % face.mode)
897         if face.mode & Mesh.FaceModes.TWOSIDE:
898             print("Debug: face.mode twosided")
899
900         print("Debug: face.transp=0x%x (enum)" % face.blend_type)
901         if face.blend_type == Mesh.FaceTranspModes.SOLID:
902             print("Debug: face.transp.SOLID")
903
904         if face.image:
905             print("Debug: face.image=%s" % face.image.name)
906         print("Debug: face.materialIndex=%d" % face.materialIndex)
907
908     # XXX not used
909     # def getVertexColorByIndx(self, mesh, indx):
910     #   c = None
911     #   for face in mesh.faces:
912     #           j=0
913     #           for vertex in face.v:
914     #                   if vertex.index == indx:
915     #                           c=face.col[j]
916     #                           break
917     #                   j=j+1
918     #           if c: break
919     #   return c
920
921     def meshToString(self,mesh):
922         # print("Debug: mesh.hasVertexUV=%d" % mesh.vertexColors)
923         print("Debug: mesh.faceUV=%d" % (len(mesh.uv_textures) > 0))
924         # print("Debug: mesh.faceUV=%d" % mesh.faceUV)
925         print("Debug: mesh.hasVertexColours=%d" % (len(mesh.vertex_colors) > 0))
926         # print("Debug: mesh.hasVertexColours=%d" % mesh.hasVertexColours())
927         print("Debug: mesh.vertices=%d" % len(mesh.vertices))
928         print("Debug: mesh.faces=%d" % len(mesh.faces))
929         print("Debug: mesh.materials=%d" % len(mesh.materials))
930
931     def rgbToFS(self, c):
932         s="%s %s %s" % (round(c[0]/255.0,self.cp),
933                         round(c[1]/255.0,self.cp),
934                         round(c[2]/255.0,self.cp))
935
936         # s="%s %s %s" % (
937         #       round(c.r/255.0,self.cp),
938         #       round(c.g/255.0,self.cp),
939         #       round(c.b/255.0,self.cp))
940         return s
941
942     def computeDirection(self, mtx):
943         x,y,z=(0,-1.0,0) # point down
944
945         ax,ay,az = (MATWORLD * mtx).to_euler()
946
947         # ax *= DEG2RAD
948         # ay *= DEG2RAD
949         # az *= DEG2RAD
950
951         # rot X
952         x1=x
953         y1=y*math.cos(ax)-z*math.sin(ax)
954         z1=y*math.sin(ax)+z*math.cos(ax)
955
956         # rot Y
957         x2=x1*math.cos(ay)+z1*math.sin(ay)
958         y2=y1
959         z2=z1*math.cos(ay)-x1*math.sin(ay)
960
961         # rot Z
962         x3=x2*math.cos(az)-y2*math.sin(az)
963         y3=x2*math.sin(az)+y2*math.cos(az)
964         z3=z2
965
966         return [x3,y3,z3]
967
968
969     # swap Y and Z to handle axis difference between Blender and VRML
970     #------------------------------------------------------------------------
971     def rotatePointForVRML(self, v):
972         return v[0], v[2], -v[1]
973
974     # For writing well formed VRML code
975     #------------------------------------------------------------------------
976     def writeIndented(self, s, inc=0):
977         if inc < 1:
978             self.indentLevel = self.indentLevel + inc
979
980         spaces=""
981         for x in range(self.indentLevel):
982             spaces = spaces + "\t"
983         self.file.write(spaces + s)
984
985         if inc > 0:
986             self.indentLevel = self.indentLevel + inc
987
988     # Converts a Euler to three new Quaternions
989     # Angles of Euler are passed in as radians
990     #------------------------------------------------------------------------
991     def eulerToQuaternions(self, x, y, z):
992         Qx = [math.cos(x/2), math.sin(x/2), 0, 0]
993         Qy = [math.cos(y/2), 0, math.sin(y/2), 0]
994         Qz = [math.cos(z/2), 0, 0, math.sin(z/2)]
995
996         quaternionVec=[Qx,Qy,Qz]
997         return quaternionVec
998
999     # Multiply two Quaternions together to get a new Quaternion
1000     #------------------------------------------------------------------------
1001     def multiplyQuaternions(self, Q1, Q2):
1002         result = [((Q1[0] * Q2[0]) - (Q1[1] * Q2[1]) - (Q1[2] * Q2[2]) - (Q1[3] * Q2[3])),
1003                   ((Q1[0] * Q2[1]) + (Q1[1] * Q2[0]) + (Q1[2] * Q2[3]) - (Q1[3] * Q2[2])),
1004                   ((Q1[0] * Q2[2]) + (Q1[2] * Q2[0]) + (Q1[3] * Q2[1]) - (Q1[1] * Q2[3])),
1005                   ((Q1[0] * Q2[3]) + (Q1[3] * Q2[0]) + (Q1[1] * Q2[2]) - (Q1[2] * Q2[1]))]
1006
1007         return result
1008
1009     # Convert a Quaternion to an Angle Axis (ax, ay, az, angle)
1010     # angle is in radians
1011     #------------------------------------------------------------------------
1012     def quaternionToAngleAxis(self, Qf):
1013         scale = math.pow(Qf[1],2) + math.pow(Qf[2],2) + math.pow(Qf[3],2)
1014         ax = Qf[1]
1015         ay = Qf[2]
1016         az = Qf[3]
1017
1018         if scale > .0001:
1019             ax/=scale
1020             ay/=scale
1021             az/=scale
1022
1023         angle = 2 * math.acos(Qf[0])
1024
1025         result = [ax, ay, az, angle]
1026         return result
1027
1028 ##########################################################
1029 # Callbacks, needed before Main
1030 ##########################################################
1031
1032 def save(operator, context, filepath="",
1033           use_apply_modifiers=False,
1034           use_triangulate=False,
1035           use_compress=False):
1036
1037     if use_compress:
1038         if not filepath.lower().endswith('.x3dz'):
1039             filepath = '.'.join(filepath.split('.')[:-1]) + '.x3dz'
1040     else:
1041         if not filepath.lower().endswith('.x3d'):
1042             filepath = '.'.join(filepath.split('.')[:-1]) + '.x3d'
1043
1044     scene = context.scene
1045     world = scene.world
1046
1047     if bpy.ops.object.mode_set.poll():
1048         bpy.ops.object.mode_set(mode='OBJECT')
1049
1050     # XXX these are global textures while .Get() returned only scene's?
1051     alltextures = bpy.data.textures
1052     # alltextures = Blender.Texture.Get()
1053
1054     wrlexport = x3d_class(filepath)
1055     wrlexport.export(scene,
1056                      world,
1057                      alltextures,
1058                      EXPORT_APPLY_MODIFIERS=use_apply_modifiers,
1059                      EXPORT_TRI=use_triangulate,
1060                      )
1061
1062     return {'FINISHED'}
1063