io_scene_x3d — Complete Code Breakdown

Blender X3D/VRML2 Import-Export Addon · v2.3.1 (legacy) → v2.5.x (extensions repo) · GPL-2.0

What this addon does

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.

Import: .x3d .wrl Export: .x3d Optional H3D shader export VRML2 parser included

File structure

__init__.py292 lines — Blender operator registration, UI panels, entry points
export_x3d.py~1590 lines — scene-to-X3D serialiser, geometry, lights, materials
import_x3d.py~3640 lines — dual-format parser (X3D XML + VRML2 text), Blender object builder

Supported X3D nodes (export)

TransformGroupShapeAppearanceMaterial IndexedFaceSetIndexedTriangleSetCoordinateNormal TextureCoordinateImageTexturePointLightDirectionalLight SpotLightViewpointFogNavigationInfo BackgroundCollision
Known gaps: no multi-material per mesh support; no multiple UV channels per mesh; no animation export; texture export limited (PBR nodes not mapped — major extension point)

Supported X3D/VRML nodes (import)

TransformGroupShapeAppearanceMaterial ImageTexturePixelTextureIndexedFaceSetIndexedLineSet PointSetBoxConeCylinderSphere ElevationGridExtrusionInlineSwitch PointLightDirectionalLightSpotLight DEF/USEROUTEPROTO/EXTERNPROTO

Role of __init__.py

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.

Classes registered

ImportX3D
Operator (import_scene.x3d). Handles .x3d and .wrl files. Calls import_x3d.load()
bl_idname: import_scene.x3d
filter_glob: *.x3d;*.wrl
execute(): builds keyword dict from properties, constructs axis_conversion global_matrix, calls import_x3d.load(context, **keywords)
Key point: uses @orientation_helper(axis_forward='Z', axis_up='Y') decorator — default Blender Y-up, forward Z
ExportX3D
Operator (export_scene.x3d). Delegates to export_x3d.save(). Exposes all export properties.
bl_idname: export_scene.x3d
Properties: use_selection, use_mesh_modifiers, use_triangulate, use_normals, use_compress, use_hierarchy, name_decorations, use_h3d, global_scale, path_mode
execute(): builds global_matrix via axis_conversion @ Matrix.Scale(global_scale, 4), calls export_x3d.save(context, **keywords)
UI Panels (×4)
X3D_PT_export_include, _transform, _geometry; X3D_PT_import_transform. FILE_BROWSER region panels.
Pattern: All use poll() to check operator bl_idname. Parented to FILE_PT_operator.
Export Include: use_selection, use_hierarchy, name_decorations, use_h3d
Export Transform: global_scale, axis_forward, axis_up
Export Geometry: use_mesh_modifiers, use_triangulate, use_normals, use_compress
Import Transform: axis_forward, axis_up
register() / unregister()
Standard Blender addon lifecycle. Appends menu items to TOPBAR_MT_file_import and _export.
Iterates 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.

Export property reference

use_selectionBool · export only selected objects
use_mesh_modifiersBool · apply modifiers before export
use_triangulateBool · write IndexedTriangleSet instead of IndexedFaceSet
use_normalsBool · include Normal node in geometry
use_compressBool · gzip compress output (.x3dz)
use_hierarchyBool · preserve parent-child Transform nesting
name_decorationsBool · prefix DEF IDs with CA_, OB_, ME_, etc.
use_h3dBool · emit H3D GLSL shader extensions
global_scaleFloat 0.01–1000 · uniform scale multiplier
path_modeEnum · AUTO/ABSOLUTE/RELATIVE/COPY/STRIP for texture paths

export_x3d.py — top-level structure

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)

clamp_color(col)
Clamps each channel to [0,1]. Guards against Blender's HDR color values.
Returns tuple. Used by all light and material colour writes.
clean_def(txt)
Sanitises a string into a valid X3D DEF identifier. Replaces ~60 illegal chars with underscores.
Prepends '_' if starts with digit. Uses str.translate() with a 60-entry table. Covers control chars, spaces, quotes, brackets, backslash. Critical: X3D DEF names have strict grammar; bad names silently break USE references in viewers.
build_hierarchy(objects)
Builds parent→children tree, skipping parents not in the export set.
Uses a local 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_direction_neg_z(matrix)
Extracts the -Z world direction from a matrix. Used for light orientation.
Computes (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())

writeHeader(ident)
Writes XML declaration, DOCTYPE, X3D root, head meta tags, Scene tag.
Outputs X3D 3.0 Immersive profile by default; H3DAPI profile when use_h3d. Writes meta filename and generator (Blender version). If use_h3d, inserts TransformInfo DEF="TOP_LEVEL_TI". Returns incremented ident string.
writeViewpoint(ident, obj, matrix, scene)
Exports a Camera as an X3D Viewpoint node.
Decomposes matrix to loc+rot+scale. Converts quaternion to axis-angle via rot.to_axis_angle(). Uses obj.data.angle for fieldOfView. DEF name prefixed with CA_.
writeTransform_begin / _end
Wraps child nodes in a Transform with translation/scale/rotation decomposed from the object matrix.
Decomposes matrix → loc, rot (quaternion→axis-angle), scale. Writes 6-decimal floats. Increments ident. _end decrements ident and closes tag. Core of hierarchy export.
writeSpotLight / writeDirectionalLight / writePointLight
Three light type handlers. Each writes the corresponding X3D light node.
SpotLight: computes beamWidth = spot_size*0.37, cutOffAngle = beamWidth*1.3, radius = lamp.distance*cos(beamWidth). All clamp energy to [0,1] by dividing by 1.75. ambientIntensity is computed from world.ambient_color but the conditional is `if world and 0:` — always zero in practice (dead code). Intentional or oversight.
writeIndexedFaceSet()
The largest function ~500 lines. Exports mesh geometry as IndexedFaceSet or IndexedTriangleSet.
Key steps:
1. Generates unique DEF IDs for obj, mesh, group, coords, normals
2. Checks for COLLISION modifier → wraps in Collision node
3. Uses mesh.tag to deduplicate mesh data (USE reference if already written)
4. Groups polygons by (material_index, image) pairs
5. For each group writes a Shape → Appearance → Material/ImageTexture → IndexedFaceSet
6. Vertex dedup: builds vertex_hash[vidx][key] where key=(normal, uv, col tuple) per loop
7. Handles USE_TRIANGULATE path via loop_triangles
8. Writes per-vertex or per-face vertex colours
Notable: creaseAngle not supported for IndexedTriangleSet, forces normals in that case
writeBackground(ident, world, world_id)
Exports Blender World horizon/zenith as X3D Background node.
Writes skyColor, groundColor from world.horizon_color / world.zenith_color. Single colour each (no gradient skybox). Wrapped in DEF for USE.
h3d_shader_glsl_frag_patch()
Post-processes GLSL fragment shader files for H3D compatibility.
Reads a GLSL .frag file, injects global var declarations after "void main(void)", patches light_visibility_* calls to transform world-space positions using H3D's view_matrix. Hacky string manipulation — searches for specific function call patterns and rewrites the argument lists. Only active when use_h3d=True.

UUID / DEF name system

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.

import_x3d.py — dual-format parser

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.

Parsing pipeline

.x3d / .wrl file
detect format
VRML: vrmlFormat() pre-processor
build vrmlNode tree
importScene()
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

vrmlFormat(data)
Multi-pass text normaliser. Produces one token-per-line canonical form that the node parser can handle.
Stage 1: Strip comments (# outside strings)
Stage 2: EXTRACT_STRINGS — extract all quoted string literals, replace with empty "" placeholders (preserves URL content which may contain commas/braces)
Stage 3: Normalise braces/brackets — replace {, }, [, ] with \n...\n versions
Stage 4: vrml_split_fields() — re-split so each property is on its own line
Stage 5: Restore extracted strings into their placeholders
Returns list of non-empty lines.
vrml_split_fields(value)
Splits a VRML token list into separate field entries. Handles DEF/USE keywords specially.
iskey() detects field-name tokens (starts with alpha, not TRUE/FALSE). Iterates tokens, collecting a field_context; flushes to field_list when a new key is found after a value. Handles DEF/USE pairs as a unit (two tokens). Returns list of token lists.

Node parser

vrmlNode (class)
Core IR node. Holds id, fields, children, array_data, DEF/PROTO/ROUTE namespaces. Bridges both X3D XML and VRML text.
Slots: id, fields, proto_node, proto_field_defs, proto_fields, node_type, parent, children, array_data, reference, lineno, filename, blendObject, blendData, DEF_NAMESPACE, ROUTE_IPO_NAMESPACE, PROTO_NAMESPACE, x3dNode, parsed

Three node types: NODE_NORMAL ({}), NODE_ARRAY ([]), NODE_REFERENCE (USE)

Namespace chain: DEF_NAMESPACE, PROTO_NAMESPACE, ROUTE_IPO_NAMESPACE stored only on root nodes (identified by having a filename). Non-root nodes walk up parent chain via getDefDict(), getProtoDict(), getRouteIpoDict().

parsed field: Stores the built Blender object so mesh/material data can be reused when the same DEF is USEd multiple times.
getNodePreText / is_nodeline / is_numline
Line-classification helpers used by the recursive VRML parser to decide if a line begins a node, array, or numeric data.
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

importScene(vrml_node, bpyscene)
Top-level walker. Recursively dispatches node types to specialised import functions.
Iterates vrml_node.children. Dispatches by node name (e.g. 'Transform', 'Shape', 'PointLight', etc.) to the appropriate importXxx() function. Handles coordinate system conversion via global_matrix. Manages collection linking for imported objects.
appearance_Create()
Builds a Blender material + optional image texture from an X3D Appearance/Material node.
Reads diffuseColor, specularColor, emissiveColor, shininess, transparency, ambientIntensity from Material node. Optionally reads ImageTexture or PixelTexture. Uses texture_cache and material_cache dicts to reuse already-built materials/images. Creates Blender Material with Principled BSDF or simple diffuse depending on Blender version.
importMesh / mesh_*
Builds bpy.data.meshes from IndexedFaceSet, IndexedTriangleSet, Box, Sphere, Cylinder, Cone, ElevationGrid, Extrusion, PointSet, IndexedLineSet.
For IndexedFaceSet: reads coordIndex, reads Coordinate point, optionally reads Normal, TextureCoordinate, Color. Builds mesh via bpy.data.meshes.new(), assigns vertices, loops, polygons. Handles -1 polygon terminator in coordIndex. For primitives (Box/Sphere/etc.), builds the geometry from scratch using math. ElevationGrid generates a height-map grid. Extrusion sweeps a 2D cross-section along a spine.
importLamp (Point/Directional/Spot)
Creates bpy.data.lights from X3D light nodes. Maps intensity, color, radius, beamWidth/cutOffAngle.
SpotLight: reads beamWidth → spot_size, cutOffAngle → spot_blend. PointLight: reads radius → cutoff_distance. DirectionalLight: creates SUN type. All map intensity * color → light energy. on=False → object hide.

DEF / USE system

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 / PROTO support

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.

ROUTE animation data is parsed into ROUTE_IPO_NAMESPACE but animation import is not fully implemented — it's a known TODO in the source comments.

Execution flow — Export

File → Export → X3D
ExportX3D.execute()
build global_matrix
export_x3d.save()
open file
export()
writeHeader()
write world/fog/navinfo
iterate objects
↓ (per object)
writeTransform_begin()
dispatch by type (MESH/CAMERA/LIGHT)
writeTransform_end()
↓ (MESH path)
writeIndexedFaceSet()
group polys by material+image
per-group: Shape/Appearance/Material/IFS
copy_set path handling
writeFooter()
path_reference_mode file copy

Execution flow — Import

File → Import → X3D/VRML2
ImportX3D.execute()
import_x3d.load()
detect XML vs VRML
↓ VRML path↓ X3D path
read file text
minidom.parse()
vrmlFormat() normalise
DOM → vrmlNode tree
VRML → vrmlNode tree
↓ (both merge here)
importScene(root, bpyscene)
recursive node dispatch
↓ (per node type)
importMesh / importLamp / importCamera / etc.
bpy.data.* creation
link to scene collection
apply global_matrix

Coordinate system handling

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.

Key extension points for your custom code

These are the hooks, gaps, and patterns you'll want to understand for extending or integrating this addon.

Texture / PBR export (gap)
The exporter has no PBR material node support. A major extension point.
Current state: writeIndexedFaceSet() groups polygons by material + image, but the material writing only handles basic Phong properties (diffuse, specular, emissive, shininess, transparency). PBR nodes (Principled BSDF) are not traversed.

Extension: In the per-material Shape write, after the Material node, add a custom material inspector that walks the node_tree of the Blender material and maps Principled BSDF inputs → X3D v4 PhysicalMaterial fields (baseColor, metallic, roughness, etc.).
Animation export (gap)
No animation export. X3D has TimeSensor, PositionInterpolator, OrientationInterpolator, etc.
Extension approach: After writing each Transform node, check if the corresponding object has animation data. Bake the action to a set of frames, write X3D TimeSensor + PositionInterpolator/OrientationInterpolator/ScalarInterpolator nodes, and connect with ROUTE statements. The import side has ROUTE_IPO_NAMESPACE infrastructure already but unused.
Collection-based export (gap)
Current exporter treats all objects flat (or with object parent hierarchy). No Blender Collection → X3D Group mapping.
The newer extensions version (v2.5+) adds per-collection export modes. To extend the legacy version: modify the object iteration in export() to group objects by collection, wrapping each collection in a named X3D Group node before writing its child objects.
Inline / external file export (gap)
Exporter always writes everything inline. X3D Inline node references external files.
Could be extended to write large meshes to separate .x3d files and reference them via Inline nodes. Useful for web streaming. The importer already handles reading Inline nodes.
H3D shader path (use_h3d=True)
Special H3D profile output. Writes GPU shader export and patches GLSL frag files.
Flow: use_h3d=True triggers: (1) gpu.export_shader() call per material, (2) writes X3D ComposedShader nodes with GLSL vert/frag, (3) post-processes .frag files via h3d_shader_glsl_frag_patch() to inject view_matrix transforms for light calculations. The Blender gpu module changed significantly in 3.x — this path may be broken in modern Blender.
Vertex colour pipeline
Both import and export handle vertex colours. Understanding the loop vs vertex distinction matters.
Export: calc_vertex_color() checks if each vertex has one consistent colour across all its loops. If yes, uses per-vertex Color node (is_col_per_vertex=True). If not, uses per-face-vertex (loop-indexed) Color. X3D ColorRGBA vs Color depends on whether alpha varies.

Import: Color/ColorRGBA read from X3D mapped back to Blender vertex_colors attribute layer.
Mesh deduplication (mesh.tag)
The exporter deduplicates identical meshes using mesh.tag = True and USE references.
Before exporting geometry, checks 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.

Gotchas when extending

  • The ambient intensity code in light writers has 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.
  • The H3D gpu.export_shader() API was removed in Blender 3.x. The h3d path will crash on modern Blender without a reimplementation using the new GPU shader introspection APIs.
  • The vertex hash key in writeIndexedFaceSet uses tuples of (normal, uv, color) — changing any of these per loop creates a new X3D vertex. This is correct for X3D but means vertex counts can explode. Be aware when adding new per-loop attributes.
  • The import parser uses a module-level lines list — a global that's set once per parse. Not thread-safe.
  • The VRML pre-processor assumes EXTRACT_STRINGS=True always (the False branch is present but unreachable). The string extraction is needed for URLs containing commas or brackets.