Blender X3D/VRML2 Import-Export Addon · v2.3.1 (legacy) → v2.5.x (extensions repo) · GPL-2.0
Bidirectional bridge between Blender and the X3D (Extensible 3D) and VRML2/VRML97 open web3D formats. Original authors: Campbell Barton, Bart, Bastien Montagne, Seva Alekseyev. Now maintained by the Web3D Consortium.
TransformGroupShapeAppearanceMaterial
IndexedFaceSetIndexedTriangleSetCoordinateNormal
TextureCoordinateImageTexturePointLightDirectionalLight
SpotLightViewpointFogNavigationInfo
BackgroundCollision
TransformGroupShapeAppearanceMaterial
ImageTexturePixelTextureIndexedFaceSetIndexedLineSet
PointSetBoxConeCylinderSphere
ElevationGridExtrusionInlineSwitch
PointLightDirectionalLightSpotLight
DEF/USEROUTEPROTO/EXTERNPROTO
The registration hub. It never does 3D math — it only wires Blender's operator system to the two worker modules and defines the UI sidebar panels that appear in the File Browser during export/import.
import_x3d.load(context, **keywords)export_x3d.save(context, **keywords)classes tuple calling bpy.utils.register_class(). On unregister, removes menu items first then unregisters classes in order. Hot-reload pattern: checks if "bpy" in locals() and importlib.reload()s submodules — so you can F8 reload scripts mid-session.One giant export() function (≈1400 lines) with all write functions defined as closures inside it, plus a thin save() entry-point that opens the file and calls export(). The closure design gives every inner function direct access to fw (file.write), all UUID caches, and all export flags with zero argument passing.
Utility functions (module-level)
par_lookup dict. test_parent() walks up obj.parent chain until it finds a parent in the export objects set. Returns list of (obj, children) tuples rooted at None (top-level objects). Used when use_hierarchy=True.(matrix.to_3x3() @ Vector(0,0,-1)).normalized(). Returns xyz tuple. Blender lights point down -Z local axis; X3D lights use a 'direction' field in world space.Write functions (closures inside export())
To avoid duplicate DEF IDs in the X3D output, the exporter maintains per-type caches:
uuid_cache_object uuid_cache_light uuid_cache_view
uuid_cache_mesh uuid_cache_material uuid_cache_image uuid_cache_world
With name_decorations=True, each type gets its own dict (safe for collision across types). With False, all share one dict. Uniqueness enforced by bpy_extras.io_utils.unique_name().
Prefix constants: CA_ cameras, OB_ objects, ME_ meshes, IM_ images, WO_ world, MA_ materials, LA_ lights, group_ group wrappers.
The largest and most complex file. Handles two completely different syntaxes — X3D's XML and VRML2's curly-brace text format — by converting both into a shared internal vrmlNode tree, then walking that tree to build Blender objects.
X3D XML files are parsed with Python's xml.dom.minidom, then each DOM element is wrapped in a vrmlNode. VRML text files go through a multi-pass pre-processor first.
VRML pre-processor stages
Node parser
getNodePreText(i, words) — scans forward from line i collecting words until it finds '{' (NODE_NORMAL) or detects a USE keyword (NODE_REFERENCE). Returns (node_type, next_line_index).is_nodeline(i, words) — calls getNodePreText, validates that all collected words are alphabetic (not numeric), handles PROTO/EXTERNPROTO specially.is_numline(i) — fast float-parse check. Tries float() on the first token (skipping leading ', '). Used to detect raw numeric array data lines.
Scene builder functions
X3D's instancing system maps directly to the import. Each node with a DEF="name" attribute is stored in the root's DEF_NAMESPACE dict. A USE="name" node is a NODE_REFERENCE that looks up the previously parsed node and reuses its .parsed result (the already-built Blender object/mesh). This is how X3D achieves geometry instancing without re-building identical meshes.
Inline nodes trigger a recursive file load. Each inline gets its own root vrmlNode with its own DEF_NAMESPACE so names don't collide across files. PROTO definitions are stored in PROTO_NAMESPACE; proto instances resolve field IS-mappings at instantiation time.
X3D uses a right-handed, Y-up coordinate system. Blender uses right-handed Z-up. The global_matrix (built from axis_conversion() in __init__.py) handles this. Default settings: axis_forward='Z', axis_up='Y' converts Blender Z-up to X3D Y-up on export, and reverses on import.
Internally the exporter applies global_matrix @ obj.matrix_world for each object before decomposing to translation/rotation/scale for the Transform node.
These are the hooks, gaps, and patterns you'll want to understand for extending or integrating this addon.
if mesh.tag: — if True, writes a USE reference to the mesh_id_group. Otherwise writes the full geometry and sets mesh.tag=True. At end of export, all tags must be reset. This is why the exporter calls depsgraph.update() — to work with evaluated meshes that have unique mesh data even for linked objects.if world and 0: — a deliberate short-circuit. Both branches exist but the amb_intensity is always 0. If you need ambient, fix this conditional.lines list — a global that's set once per parse. Not thread-safe.