Building a custom GLB import pipeline in Godot 4 puts you in full control — automatic LOD generation, collision meshes, shadow proxies, and material overrides applied the moment a file lands in your project. EditorImportPlugin is the hook that makes it possible.
What EditorImportPlugin does and when to use it
Godot's built-in GLB importer handles most assets well, but it runs identically on every file. When you have 747 assets from a source like the BitSoul marketplace and need consistent treatment — stripped UV2 channels, forced double-sided materials, or named collision nodes — writing a custom importer pays for itself on the first batch import.
EditorImportPlugin is a GDScript (or C#) class that registers itself with the editor. It declares the file extensions it handles, a priority value that overrides the default importer, and a _import() method that receives the source path and writes to a .godot-internal resource file. The four methods you must implement: _get_importer_name() returns a unique reverse-domain string; _get_recognized_extensions() returns ["glb"]; _get_priority() returns a float above 1.0 to supersede the built-in GLB importer; _get_import_order() returns 0 for most cases.
@tool
extends EditorImportPlugin
func _get_importer_name(): return "com.mystudio.glb_game_ready"
func _get_recognized_extensions(): return ["glb"]
func _get_priority() -> float: return 2.0 # > 1.0 overrides the default importer
func _get_import_order() -> int: return 0
Place this script in addons/glb_importer/ alongside a plugin.cfg and enable it in Project → Project Settings → Plugins.
![]()
Implementing LOD generation in _import()
The _import() method receives the source GLB path and an options dictionary from _get_import_options(). Load the scene via GLTFDocument, walk every MeshInstance3D with a recursive helper (_get_all_children(scene)), then call generate_lods() on each ImporterMesh. Always call scene.queue_free() after packing — the editor does not garbage-collect the generated scene automatically.
func _import(source_file, save_path, options, platform_variants, gen_files) -> Error:
var doc := GLTFDocument.new()
var state := GLTFState.new()
var err := doc.append_from_file(source_file, state)
if err != OK: return err
var scene := doc.generate_scene(state)
for node in _get_all_children(scene):
if node is MeshInstance3D:
var im := node.mesh.get_importable_mesh()
if im:
im.generate_lods(options.get("lod_normal_merge_angle", 25.0),
options.get("lod_normal_split_angle", 60.0), [])
node.mesh = im.get_mesh()
var packed := PackedScene.new()
packed.pack(scene)
scene.queue_free()
return ResourceSaver.save(packed, "%s.%s" % [save_path, _get_save_extension()])
generate_lods() takes merge angle, split angle, and an optional LOD ratio array — pass [] to let Godot choose automatically (typically 0.25, 0.1, 0.04). get_importable_mesh() returns null for meshes that weren't imported as ImporterMesh, so always check before calling.
Adding automatic collision meshes
After the LOD loop, add StaticBody3D collision for each prop mesh: create a StaticBody3D, add a child CollisionShape3D with shape = node.mesh.create_convex_shape(true, true) (both flags clean and simplify the hull), set .owner = scene on both nodes, then call node.add_child(body). For concave meshes — stairs, hollow props — use create_trimesh_shape() instead; it's more accurate but more expensive at runtime.
Declaring import options for per-asset control
Return import options from _get_import_options(_path, _preset_index) as an Array[Dictionary], each entry with name and default_value keys. They appear as editable fields in the Import dock, letting artists override settings per file without touching code. Commit .import sidecar files to version control so teammates get identical results.
| Option | Default | Effect |
|---|---|---|
generate_collision | true | Adds StaticBody3D + hull shape |
lod_normal_merge_angle | 25.0 | Merge angle for LOD generation |
lod_normal_split_angle | 60.0 | Split angle for LOD generation |
force_double_sided | false | Disables back-face culling on all surfaces |
shadow_proxy_lod | 2 | Which LOD level is used as shadow caster |
![]()
Workflow checklist for shipping a custom importer
| Step | Action | Gotcha |
|---|---|---|
| Register plugin | Add plugin.cfg, enable in Project Settings | Priority must be > 1.0 to override default GLB importer |
| Define options | Return typed dicts from _get_import_options() | Missing type key causes silent failures in older 4.x builds |
| LOD generation | Call generate_lods() on ImporterMesh, reassign .mesh | Only works if the original mesh was imported as ImporterMesh; check with get_importable_mesh() |
| Collision | Use create_convex_shape(true, true) for props | Concave meshes need create_trimesh_shape() — more expensive at runtime |
| Re-import trigger | Edit .import file or call EditorFileSystem.reimport_files() | Godot does not auto-reimport after plugin code changes |
| Version control | Commit .import sidecars | Without them, collaborators fall back to default importer settings |
The BitSoul marketplace ships assets as clean GLB with consistent naming — ideal input for a pipeline like this, since node names are predictable and the plugin can branch on them (e.g., nodes prefixed UCX_ get collision-only treatment, matching Unreal conventions).
Automating import with EditorImportPlugin turns asset integration from a per-file chore into a one-time engineering task. Write it once, import 500 assets the right way every time.
This post is part of the Ultimate Guide to Free 3D Game Assets — BitSoul's complete reference for formats, texturing, rigging, optimization, and engine integration.
Need drop-in assets for this workflow? Grab the 98-model Character Pack on the BitSoul marketplace and drop them straight into your project.