Please try another browser if you have difficulties.
Blender - Export VRML
I've used Blender for making VRML objects to be imported into my favourite floor planning software RoomArranger for many years. I wrote some notes about how to do this in the RoomArranger Wiki and I've put some example pictures on my PhotoSwipe page.
Unfortunately when Blender 2.80 was released I found that they had removed the option to export VRML files. This probably isn't too surprising as VRML has been a bit out of date for many years and has largely been replaced by X3D now. Having updated the FreeCAD Python script for exporting VRML I thought I'd try converting the Blender 2.79 script to work with version 2.80. Although I don't know Blender that well and I'm pretty new to Python I've ended up with a version that seems to work well for me. However, I won't be submitting it as an official add-on as a requirement for that is that I commit to testing it against each new version of Blender. The reality is that I use Blender relatively infrequently and don't normally install every update.
Installation
Download the following file:
Then go to Edit > Preferences > Add-ons and select the option to install from the downloaded file. It needs to be activated by selecting Import-Export: VRML2 on the Community tab in the same Add-ons screen.
Usage
Access the script using File > Export > VRML2 (.wrl). You will see that the screen has changed from the 2.79 version:
In the old version ticking "Selection Only" when no meshes were actually selected would run the script and output an empty file. In my version (in Windows) it will display a message box.
The old version also required you to tick the appropriate boxes to specify if there were individual faces or vertices with different colours, or if a UV texture had been used. I removed those choices and now determine this automatically in the script. (Note that in Blender 2.80 vertex colours only get used if they are selected in an Attribute node.) Also the old version didn't really work with nodes at all so often wouldn't output any colours, and it never output any material settings for the object itself while mine does.
My search routine for colours when nodes are used might not be completely accurate, but the assumption is that objects created for VRML export probably don't have sophisticated node trees for the materials.
The Path Mode option is still shown as it defines how paths should be output if a UV texture is found (in an Image Texture node). It is mentioned here in the documentation:
My version also includes the object boundaries and dimensions as comments in the VRML file.
Python files
The initialisation file included in the download above is __init__.py:
# ##### 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 #####
bl_info = {
"name": "VRML2 (Virtual Reality Modeling Language)",
"author": "Graham O'Neill, Campbell Barton",
"version": (1, 0, 1),
"blender": (2, 80, 0),
"location": "File > Export",
"description": "Exports mesh objects to VRML2, supporting vertex and material colors",
"warning": "",
"wiki_url": "",
"category": "Import-Export",
}
if "bpy" in locals():
import importlib
if "export_vrml2" in locals():
importlib.reload(export_vrml2)
import os
import bpy
from bpy.props import (
CollectionProperty,
BoolProperty,
EnumProperty,
FloatProperty,
StringProperty,
)
from bpy_extras.io_utils import (
ExportHelper,
orientation_helper,
axis_conversion,
path_reference_mode,
)
@orientation_helper(axis_forward='Z', axis_up='Y')
class ExportVRML(bpy.types.Operator, ExportHelper):
"""Export mesh objects as a VRML2, colors and texture coordinates"""
bl_idname = "export_scene.vrml2"
bl_label = "Export VRML2"
bl_options = {'PRESET'}
filename_ext = ".wrl"
filter_glob: StringProperty(default="*.wrl", options={'HIDDEN'})
use_selection: BoolProperty(
name="Selection Only",
description="Export selected objects only",
default=True,
)
use_mesh_modifiers: BoolProperty(
name="Apply Modifiers",
description="Apply Modifiers to the exported mesh",
default=False,
)
global_scale: FloatProperty(
name="Scale",
min=0.01, max=1000.0,
default=1.0,
)
path_mode: path_reference_mode
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj is not None) and obj.type == 'MESH'
def execute(self, context):
from . import export_vrml2
from mathutils import Matrix
keywords = self.as_keywords(ignore=("axis_forward",
"axis_up",
"global_scale",
"check_existing",
"filter_glob",
))
global_matrix = axis_conversion(from_forward='Y',
from_up='Z',
to_forward=self.axis_forward,
to_up=self.axis_up,
).to_4x4() @ Matrix.Scale(self.global_scale, 4)
keywords["global_matrix"] = global_matrix
filepath = self.filepath
filepath = bpy.path.ensure_ext(filepath, self.filename_ext)
return export_vrml2.save(self, context, **keywords)
def draw(self, context):
layout = self.layout
layout.prop(self, "use_selection")
layout.prop(self, "use_mesh_modifiers")
layout.separator()
layout.prop(self, "path_mode")
layout.separator()
box = layout.box()
row = box.row()
row.prop(self, "axis_forward")
row = box.row()
row.prop(self, "axis_up")
layout.separator()
layout.prop(self, "global_scale")
def menu_func_export(self, context):
self.layout.operator(ExportVRML.bl_idname, text="VRML2 (.wrl)")
def register():
bpy.utils.register_class(ExportVRML)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
bpy.utils.unregister_class(ExportVRML)
if __name__ == "__main__":
register()
The export of the VRML is processed by export_vrml2.py:
# ##### 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 #####
import bpy
import bpy_extras
import bmesh
import os
from bpy_extras import object_utils
from collections import namedtuple
from platform import system
from ctypes import *
Bounds = namedtuple('Bounds', ['mn', 'mx'])
objBounds = Bounds(mn=[None, None, None], mx=[None, None, None])
def valToStr(val, dec=6):
fmt = '%.'+str(dec)+'f'
ret = (fmt % val).rstrip('0').rstrip('.')
if ret == '-0': ret='0'
return ret
def minmax(p, bnd):
for i in range(3):
if bnd.mn[i] is None or p[i] < bnd.mn[i]: bnd.mn[i] = p[i]
if bnd.mx[i] is None or p[i] > bnd.mx[i]: bnd.mx[i] = p[i]
return bnd
def materialColourRGB(mat):
clr = [0,0,0]
# get non-node colour
if not mat.use_nodes:
for i in range(3):
clr[i] = mat.diffuse_color[i]
return clr
# try to get node colour
nodes = mat.node_tree.nodes
for node in nodes:
# print(node.bl_idname)
# print(dir(node))
if node.name in ["Material Output", "Emission", "Holdout", "Mix Shader"]:
continue
if node.inputs and len(node.inputs) >= 3:
for i in range(3):
clr[i] = node.inputs[0].default_value[i]
return clr
def materialColour(mat):
clr = [0]*12 # [0-diffR,1-diffG,2-diffB, 3-tran, 4-specR,5-specG,6-specB, 7-shin, 8-emitR,9-emitG,10-emitB, 11-powr]
# get non-node colour
if not mat.use_nodes:
for i in range(3):
clr[i] = mat.diffuse_color[i] # diff
clr[3] = 1 - mat.diffuse_color[3] # tran
i = 4
for c in ['r','g','b']:
clr[i] = getattr(mat.specular_color, c) # spec
i += 1
clr[7] = mat.specular_intensity # shin
return clr
# try to get node colour
nodes = mat.node_tree.nodes
for node in nodes:
# print(node.bl_idname)
# print(dir(node))
if node.name in ["Material Output", "Holdout", "Mix Shader"]:
continue
if node.name == "Emission":
for i in range(3):
clr[i+8] = node.inputs[0].default_value[i] # emit
clr[3] = 1 - node.inputs[0].default_value[3] # tran
clr[11] = node.inputs[1].default_value # powr
continue
if node.inputs and len(node.inputs) >= 3:
for i in range(3):
clr[i] = node.inputs[0].default_value[i] # diff
if len(node.inputs) >= 4:
clr[3] = 1 - node.inputs[0].default_value[3] # tran
if node.name == "Principled BSDF":
for i in range(3):
clr[i+4] = node.inputs[3].default_value[i] # spec
clr[7] = node.inputs[5].default_value # shin
for i in range(3):
clr[i+8] = node.inputs[17].default_value[i] # emit
clr[11] = 1 # powr
elif node.name == "Principled Volume":
for i in range(3):
clr[i+8] = node.inputs[7].default_value[i] # emit
clr[11] = 1 # powr
elif node.name == "Specular":
for i in range(3):
clr[i+4] = node.inputs[1].default_value[i] # spec
for i in range(3):
clr[i+8] = node.inputs[3].default_value[i] # emit
clr[11] = 1 # powr
return clr
def write_VRML(fw, bm,
colour_typ, colour_obj, colour_fac, colour_img,
path_mode, copy_set):
global objBounds
base_src = os.path.dirname(bpy.data.filepath)
base_dst = os.path.dirname(fw.__self__.name)
fw('Shape {\n')
fw('\tappearance Appearance {\n')
if colour_typ == 'TEXTURE':
fw('\t\ttexture ImageTexture {\n')
filepath = colour_img.filepath
filepath_full = os.path.normpath(bpy.path.abspath(filepath, library=colour_img.library))
filepath_ref = bpy_extras.io_utils.path_reference(filepath_full, base_src, base_dst, path_mode, "textures", copy_set, colour_img.library)
filepath_base = os.path.basename(filepath_full)
images = [
filepath_ref,
filepath_base,
]
if path_mode != 'RELATIVE':
images.append(filepath_full)
fw('\t\t\turl [ %s ]\n' % " ".join(['"%s"' % f for f in images]))
del images
del filepath_ref, filepath_base, filepath_full, filepath
fw('\t\t}\n') # end 'ImageTexture'
else:
fw('\t\tmaterial Material {\n')
if colour_typ == 'OBJECT':
fw("\t\t\tdiffuseColor %f %f %f\n" % (colour_obj[0], colour_obj[1], colour_obj[2]))
if colour_obj[8] != 0 or colour_obj[9] != 0 or colour_obj[10] != 0:
fw("\t\t\temissiveColor %f %f %f\n" % (colour_obj[8], colour_obj[9], colour_obj[10]))
if colour_obj[4] != 0 or colour_obj[5] != 0 or colour_obj[6] != 0:
fw("\t\t\tspecularColor %f %f %f\n" % (colour_obj[4], colour_obj[5], colour_obj[6]))
if colour_obj[11] != 0:
fw("\t\t\tambientIntensity %f\n" % colour_obj[11])
fw("\t\t\tshininess %f\n" % colour_obj[7])
if colour_obj[3] != 0:
fw("\t\t\ttransparency %f\n" % colour_obj[3])
fw('\t\t}\n') # end 'Material'
fw('\t}\n') # end 'Appearance'
fw('\tgeometry IndexedFaceSet {\n')
fw('\t\tcoord Coordinate {\n')
fw('\t\t\tpoint [')
v = None
vDel = ''
for v in bm.verts:
fw(vDel + valToStr(v.co[0]) + ' ' + valToStr(v.co[1]) + ' ' + valToStr(v.co[2]))
objBounds = minmax(v.co, objBounds)
vDel = ','
del v, vDel
fw(']\n') # end 'point[]'
fw('\t\t}\n') # end 'Coordinate'
fw('\t\tcoordIndex [ ')
f = fv = None
for f in bm.faces:
fv = f.verts[:]
fw("%d %d %d -1 " % (fv[0].index, fv[1].index, fv[2].index))
del f, fv
fw(']\n') # end 'coordIndex[]'
if colour_typ == 'FACES':
fw('\t\tcolorPerVertex FALSE\n')
fw('\t\tcolor Color {\n')
fw('\t\t\tcolor [')
c = None
cDel = ''
for c in colour_fac:
fw(cDel + valToStr(c[0]) + ' ' + valToStr(c[1]) + ' ' + valToStr(c[2]))
cDel = ','
del c, cDel
fw(']\n') # end 'color[]'
fw('\t\t}\n') # end 'Color'
fw('\t\tcolorIndex [ ')
i = None
for f in bm.faces:
i = f.material_index
if i >= len(colour_fac):
i = 0
fw("%d " % i)
del i
fw(']\n') # end 'colorIndex[]'
elif colour_typ == 'VERTICES':
fw('\t\tcolorPerVertex TRUE\n')
fw('\t\tcolor Color {\n')
fw('\t\t\tcolor [')
v = None
cDel = ''
color_layer = bm.loops.layers.color.active
assert(color_layer is not None)
for v in bm.verts:
try:
l = v.link_loops[0]
except:
l = None
if l is None:
fw(cDel+'0.0 0.0 0.0')
else:
c = l[color_layer]
fw(cDel + valToStr(c[0]) + ' ' + valToStr(c[1]) + ' ' + valToStr(c[2]))
cDel = ','
del v, cDel
fw(']\n') # end 'color[]'
fw('\t\t}\n') # end 'Color'
elif colour_typ == 'TEXTURE':
fw('\t\ttexCoord TextureCoordinate {\n')
fw('\t\t\tpoint [')
v = None
vDel = ''
uv_layer = bm.loops.layers.uv.active
assert(uv_layer is not None)
for f in bm.faces:
for l in f.loops:
fw(vDel + valToStr(l[uv_layer].uv[0],4) + ' ' + valToStr(l[uv_layer].uv[1],4))
vDel = ','
del f, vDel
fw(']\n') # end 'point[]'
fw('\t\t}\n') # end 'TextureCoordinate'
fw('\t\ttexCoordIndex [ ')
i = None
for i in range(0, len(bm.faces) * 3, 3):
fw("%d %d %d -1 " % (i, i + 1, i + 2))
del i
fw(']\n') # end 'coordIndex[]'
fw('\t}\n') # end 'IndexedFaceSet'
fw('}\n') # end 'Shape'
def save_object(obj, global_matrix, use_mesh_modifiers,
path_mode, copy_set,
fw, depsgraph):
assert(obj.type == 'MESH')
is_editmode = (obj.mode == 'EDIT')
if is_editmode:
bpy.ops.object.editmode_toggle()
if use_mesh_modifiers:
obj_eval = obj.evaluated_get(depsgraph)
msh = obj_eval.to_mesh()
else:
msh = obj.data
bm = bmesh.new()
bm.from_mesh(msh)
# triangulate first so tessellation matches the view-port.
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.transform(global_matrix @ obj.matrix_world)
colour_typ = 'NONE'
colour_obj = None
colour_fac = None
colour_img = None
obj_mat = None
ls = len(obj.material_slots)
if bm.loops.layers.color.active is not None:
colour_typ = 'VERTICES'
elif ls != 0:
slot_used = [False] * ls
for f in msh.polygons: # iterate over faces
slot_used[f.material_index] = True
for i in range(ls):
if slot_used[i]:
if colour_typ == 'NONE':
colour_typ = 'OBJECT'
obj_mat = obj.material_slots[i].material
else:
colour_typ = 'FACES'
break
if bm.loops.layers.uv.active is not None:
for s in obj.material_slots:
if s.material and s.material.use_nodes:
for n in s.material.node_tree.nodes:
if n.type == 'TEX_IMAGE':
colour_typ = 'TEXTURE'
colour_img = n.image
break
if colour_typ == 'OBJECT':
colour_obj = materialColour(obj_mat)
if colour_typ == 'FACES':
colour_fac = []
for i in range(ls):
if slot_used[i]:
mat = obj.material_slots[i].material
mat_rgb = materialColourRGB(mat)
else:
mat_rgb = [0,0,0]
colour_fac.append(mat_rgb)
write_VRML(fw, bm,
colour_typ, colour_obj, colour_fac, colour_img,
path_mode, copy_set)
bm.free()
if use_mesh_modifiers:
obj_eval.to_mesh_clear()
if is_editmode:
bpy.ops.object.editmode_toggle()
def save(operator, context,
filepath="",
global_matrix=None,
use_selection=False,
use_mesh_modifiers=True,
path_mode='AUTO'):
global objBounds
objBounds = Bounds(mn=[None, None, None], mx=[None, None, None])
if use_selection:
objects = [obj for obj in context.view_layer.objects
if obj.visible_get(view_layer=context.view_layer)
and obj.select_get(view_layer=context.view_layer)
and obj.type == 'MESH']
else:
objects = [obj for obj in context.view_layer.objects
if obj.visible_get(view_layer=context.view_layer)
and obj.type == 'MESH']
if len(objects) == 0 and system() == 'Windows':
user32 = windll.user32
Answer = user32.MessageBoxW(None, "No objects found for export", "Error", 0)
return {'FINISHED'}
file = open(filepath, 'w', encoding='utf-8')
fw = file.write
fw('#VRML V2.0 utf8\n')
fw('#modelled using Blender '+str(bpy.app.version[0])+'.'+str(bpy.app.version[1])+' http://blender.org\n')
if len(objects) == 0:
fw("\n#Nothing selected for export")
file.close()
return {'FINISHED'}
# create empty set to store texture filenames to copy
copy_set = set()
depsgraph = context.evaluated_depsgraph_get()
for obj in objects:
fw("\n# %r\n" % obj.name)
save_object(obj, global_matrix, use_mesh_modifiers,
path_mode, copy_set,
fw, depsgraph)
fw("\n#Object boundaries: From (")
fw(valToStr(objBounds.mn[0])+", "+valToStr(objBounds.mn[1])+", "+valToStr(objBounds.mn[2])+") to (")
fw(valToStr(objBounds.mx[0])+", "+valToStr(objBounds.mx[1])+", "+valToStr(objBounds.mx[2])+")\n")
fw("#Object dimensions: ")
fw(valToStr(abs(objBounds.mx[0]-objBounds.mn[0]))+" x ")
fw(valToStr(abs(objBounds.mx[1]-objBounds.mn[1]))+" x ")
fw(valToStr(abs(objBounds.mx[2]-objBounds.mn[2]))+"\n")
file.close()
# copy all collected files.
bpy_extras.io_utils.path_reference_copy(copy_set)
return {'FINISHED'}