Blender to SimVX (glTF)¶
SimVX imports glTF 2.0 scenes with a single call: import_gltf turns a
.gltf file into a ready-to-use Node3D hierarchy with meshes, PBR
materials, lights, cameras, skeletons, and skeletal animation clips. Blender
ships a first-class glTF exporter, so the Blender-to-SimVX pipeline is:
export from Blender, call import_gltf, add the returned node to your scene.
from simvx.graphics import import_gltf
model = import_gltf("assets/ship.gltf")
model.name = "Ship"
self.add_child(model)
The same call works on desktop (Vulkan) and in the web runtime (WebGPU); see
Web export notes for the browser subset. A missing or unreadable file logs an
error and returns an empty Node3D instead of raising, so a scene with a
broken asset still runs.
A complete runnable viewer is at examples/features/3d/model_viewer.py
(Khronos DamagedHelmet) and a skinned-animation viewer at
examples/features/3d/animated_model.py (Khronos Fox).
Recommended export settings¶
In Blender: File > Export > glTF 2.0. Settings that matter to SimVX:
Exporter setting |
Recommended |
Why |
|---|---|---|
Format |
glTF Separate ( |
Loads on desktop AND in the web runtime, and |
Transform > +Y Up |
On (default) |
glTF and SimVX share the same conventions (right-handed, Y-up, -Z forward), so no axis fix-ups happen at import. |
Include > Data > Punctual Lights |
On, if you author lights in Blender |
Exports |
Include > Data > Cameras |
On, if you author cameras in Blender |
Imports as |
Mesh > Apply Modifiers |
On |
SimVX imports the evaluated mesh only. |
Mesh > Normals |
On |
Missing normals shade flat black. |
Mesh > Tangents |
On when using normal maps |
Exported tangents drive the high-quality tangent-space normal-mapping path. Without them the renderer falls back to screen-space derivative tangents (works, slightly lower quality). |
Mesh > Attributes / Vertex Colors |
Optional |
|
Compression (Draco) |
Off |
|
Animation |
On, for rigged models |
Skinned (armature) actions import as skeletal clips. Object-level animation does not import (see Current limitations). |
What is imported¶
glTF |
SimVX |
|---|---|
Node hierarchy + names |
|
Node TRS or |
|
Mesh primitives |
|
|
Core mesh streams |
|
Tangent-space normal mapping (pipeline variant enabled per mesh) |
|
Imported and uploaded in the mesh’s extras stream; not yet consumed by the built-in PBR shader |
PBR metallic-roughness materials |
|
Alpha modes |
|
|
|
|
|
Cameras (perspective) |
|
|
|
|
Folded into the material’s emissive intensity |
Skins (joints + inverse bind matrices) |
|
Skinned animations |
|
Material mapping (Principled BSDF)¶
Blender’s Principled BSDF exports to glTF PBR metallic-roughness, which maps directly onto Material:
Principled BSDF / glTF |
|
|---|---|
Base Color factor / texture |
|
Metallic, Roughness |
|
Normal Map node |
|
Emission Color / Emission Strength sockets |
|
Occlusion (exporter’s glTF material output group) |
|
Alpha (with the material’s blend/clip mode) |
|
Backface Culling off |
|
Emissive follows the glTF formula within what the additive shader term
supports: a zero emissiveFactor (the spec default) disables the emissive
texture; a factor without a texture becomes a constant emissive colour whose
intensity carries KHR_materials_emissive_strength. A non-unit factor or
strength combined with a texture cannot be expressed and the texture is kept
unscaled.
Lights¶
glTF cone angles are radian half-angles and import converted to SimVX’s
degree half-angles; range is wired when present.
glTF stores photometric intensities (candela for point/spot, lux for
directional); SimVX converts them to its linear Light3D.intensity on
import. The conversion inverts Blender’s default (“Standard”) export math
(the CIE peak luminous efficacy 683 lm/W, plus 1/(4*pi) sr for point and
spot lamps), then anchors Blender’s factory-default lamps onto the engine’s
nominal intensity 1.0:
Blender default lamp |
glTF value |
Imported |
|---|---|---|
Sun, 1.0 W/m^2 |
683 lux |
1.0 |
Point, 1000 W |
~54351 cd |
~1.0 |
Spot, 1000 W |
~54351 cd |
~1.0 |
So a Blender scene lit with default-strength lamps imports at a sensible
brightness, and any lamp round-trips to its Blender-native energy scaled to
engine units. Files that instead carry raw glTF units (e.g. a literal
intensity of 1 cd) are physically very dim and will import correspondingly
faint, which is correct: those numbers are photometric, not engine multipliers.
Cameras¶
Perspective cameras import fully. Orthographic cameras import as a default
perspective Camera3D with a logged warning. An imported camera does not
become current automatically; activate it like any other camera.
Skeletal animation¶
The importer attaches the parsed Skeleton to each skinned
MeshInstance3D and hangs the baked clips off the imported root. Drive them
with an AnimationPlayer:
from simvx.core import AnimationPlayer
from simvx.graphics import import_gltf
model = import_gltf("assets/fox.gltf")
self.add_child(model)
skeleton = next(
n.skeleton for n in model.walk() if getattr(n, "skeleton", None)
)
player = AnimationPlayer(skeleton=skeleton)
for clip in getattr(model, "_skeletal_clips", []):
player.add_clip(clip)
player.play("Run", loop=True)
self.add_child(player)
See examples/features/3d/animated_model.py for the full pattern, including
clip switching from UI.
Cooking textures for shipping¶
For shipped builds, convert your PNG/JPG textures to GPU block-compressed KTX2 and pre-transcode them offline so load time does zero transcode work:
# 1. Author a universal UASTC .ktx2 per texture (once)
toktx --uastc --genmipmap --t2 brick.ktx2 brick.png
# 2. Reference the .ktx2 from the material / glTF image URI
# 3. Cook the scene for your shipping target
simvx cook ship.gltf -o cooked/ # desktop: explicit BC7
simvx cook ship.gltf -o cooked/ --target astc4x4 # mobile
A .gltf input carries every externally referenced buffer and image across
(relative layout preserved, so import_gltf("cooked/ship.gltf") works
as-is), transcoding each UASTC .ktx2 and copying everything else
unchanged. Web exports should keep the original UASTC sources: the browser
transcodes per-device. See Compressed Textures (BC / ASTC / ETC2) for the full
authoring, fallback, and per-platform matrix.
Blender exports PNG/JPG textures; the toktx step is how .ktx2 enters the
pipeline. Reference the .ktx2 by its plain image URI: the glTF
KHR_texture_basisu extension wrapper is not parsed (see below), but the
texture manager dispatches on the .ktx2 suffix, so a direct URI loads
through the full compressed-texture path.
Web export notes¶
import_gltf is backend-agnostic: the same call runs under Pyodide with
textures streamed through the resource channel. The browser runtime uses a
dependency-free parser that covers the common export shape (.gltf JSON +
external .bin + image files, or inline data: URIs), including skins and
skeletal animation. It does not read .glb, interleaved vertex buffers, or
sparse accessors, so export glTF Separate for web targets and bundle the
.gltf, .bin, and texture files alongside the game.
Current limitations¶
Draco / meshopt:
KHR_draco_mesh_compressionandEXT_meshopt_compressionare not supported. Export uncompressed.KHR_texture_basisuis not parsed; reference.ktx2files by plain image URI instead (loads by suffix, desktop and web)..glb: desktop only (viapygltflib); the web parser andsimvx cookneed.gltf+ external files (cookcopies a.glbunchanged without cooking its embedded images).Object animation: only skinned (armature joint) channels import as clips; TRS animation of non-joint nodes is dropped. Morph targets / shape keys are not supported.
Other material extensions (transmission, clearcoat, sheen, IOR, specular, volume, iridescence) are ignored; the core PBR metallic-roughness set imports.
One UV set per material: every map samples
TEXCOORD_0(textureInfo.texCoordselectors are ignored).TEXCOORD_1andCOLOR_0are imported and uploaded but not yet consumed by the built-in PBR shader.One UV transform per material:
KHR_texture_transformapplies material-wide, taken from the base-colour map first.Negative or near-zero scale components are not applied to imported nodes (mirrored geometry should be baked into the mesh).
Light units are converted from glTF photometric values (lux / candela) to the engine’s linear
intensity, anchored on Blender defaults (see Lights above).Sparse accessors are not supported on either parser backend.