Forge create¶
Create mjcf hierarchy relationship use forge tool.
General rules¶
- Specification compliance: these guides are the authoritative specification of the Forge tool surface — strictly follow the rules, signatures, parameter ranges and workflows documented here. Never read, search or infer behavior from the addon source code (
addon/Forge/**); source internals are not part of the interface. Runtime inspection through the MCP tools (forge_object_info,forge_scene_config, ...) is allowed. - No undocumented operations: work only through what these guides document. When a needed operation, parameter or enum value has no documented implementation, do not improvise with raw
bpyor undocumented APIs — stop and report the gap (what was attempted, what is missing) as feedback. - Main scene: the main scene must be named
Scene. Forge resolves the active scene throughscene.Main_Scene(defaults to"Scene"), e.g.bpy.data.scenes[scene.Main_Scene].Forge_Props. Create the model in the scene namedSceneonly — do not rename or delete it, and do not create objects in another scene. - Collider selection: prefer shape colliders (
BOX,CAPSULE,CYLINDER,SPHERE). Consider aMESHcollider only when shape colliders cannot meet the required collision geometry or accuracy. A mesh visual alone is not a reason to choose a mesh collider.
MCP tool workflow¶
- Prefer the dedicated MCP tools over raw
bpy: inspect withforge_blendfile_summary,forge_scene_info,forge_object_infoandforge_measure; create withforge_create_*; change types/parenting withforge_define_*; edit withforge_set_transform,forge_set_shape_size,forge_set_propertyandforge_rename; validate withforge_mesh_check. Useforge_executeonly when no dedicated tool represents the operation. - Skill gate scope: generic Blender deletion/removal operations remain available without a Forge guide. Use Forge operations for Forge hierarchy changes, especially
forge_define_parent; do not assignobj.parentdirectly when establishing a Forge parent relationship. - Every
bpy.ops.forge.*operator (signature, defaults,poll, MCP wrapper) is inventoried in theoperatorsskill —forge_skill(name="operators", section="<operator name>")for a single entry. - Follow inspect → create/edit → verify → validate → save/export. Check the main scene and existing names first; after every creation capture the returned
creatednames, inspect those exact objects, measure relevant world bounds, runforge_mesh_checkon related visuals, and useforge_screenshotwhen visual confirmation adds value. - Every MCP
select=[...]argument is ordered. The first name becomes Blender's active object and every name receives aselected_ordermatching its list index. This order determines target/reference roles for tendon, equality, sensor, light/camera targeting and parent operations; never rely onbpy.context.selected_objectsiteration order. - Before a destructive or file-writing operation, inspect its explicit targets and destination.
forge_deleterecursively removes every descendant of each named root, so inspectchildrenfirst and pass only intended roots.
Forge mjcf model types¶
In Forge treat all MJCF tags—including body, inertial, geom, joint, site, actuator, tendon, equality, contact, sensor, light, camera, frame, replicate, attach and include are treated as Blender objects, distinguished by the Forge_Model_Type attribute of the Object type.
Code example:
# register with bpy.types.Object.Forge_Model_Type
bpy.data.objects["name_of_object"].Forge_Model_Type
bpy.context.active_object.Forge_Model_Type
bpy.context.selected_objects[0].Forge_Model_Type
Forge model types are:
UNDEFINED, LINK, INERTIAL, VISUAL, COLLIDER, HFIELD, PLANE, JOINT, SITE, ACTUATOR, TENDON, EQUALITY, CONTACT, SENSOR, LIGHT, CAMERA, FRAME, REPLICATE, ATTACH, INCLUDE.
UNDEFINEDobjects are not MJCF elements and are not exported as model elements.LINKrepresents an MJCFbodyin Blender's model hierarchy.VISUAL,COLLIDER,HFIELD, andPLANEall represent MJCFgeomelements but have distinct Forge types and parameters.- The remaining types each also have their own unique set of parameters.
Hierarchy rules¶
An articulated model starts with a root link. An inertial, joint, or site must be a child of a link.
visual, collider, light, camera, frame, replicate and attach can either be children of a link, or exist as root nodes without a parent. A collider is never parented under a visual: keep it a sibling of its visual, under the same link. A collider under a visual is not exported — it vanishes from the MJCF — and forge_define_parent refuses to create that nesting.
hfield and plane exist as root nodes without a parent for now.
In mjcf actuator, tendon, equality, contact and sensor do not use a hierarchy. In Forge, they will be created as children of their corresponding target model type.
Create rules¶
Creation order:¶
- Create elements with
bpy.ops.forge.create_<type>, optionally using selected existing model elements as targets. - Adjust
location,rotation, andscaleas needed, respecting locked axes.- Do not change a component when the corresponding
lock_scale,lock_rotation, orlock_locationflag isTrue.
- Do not change a component when the corresponding
- Edit the parameters supported by the model type as needed.
File workflow and mesh preprocessing¶
forge_import_mjcf(filepath, import_material=True)wrapsbpy.ops.forge.import_mjcfand waits for its modal job. Import is destructive: it clears scene objects, collections, non-main scenes and orphaned data before rebuilding, so save first when current work matters.envhdrtextures are imported into a Blender World environment texture and assigned to the main scene.- Mesh assets support OBJ, STL and GLB. GLB accepts exactly one Mesh object; non-Mesh elements are discarded, and zero/multiple Mesh objects fail during asset loading. The Mesh node's transform is baked, file-space coordinates/UVs are preserved, and MJCF mesh
scaleis applied afterwards. MJCF defines the materials; embedded GLB materials are discarded. Missing/unsupported mesh files fail during asset loading. forge_export_mjcf(filepath, update_forge_only=False, use_glb=False, overwrite=False)wrapsbpy.ops.forge.export_mjcf, waits for completion and reports whether the XML exists. Existing destinations require explicitoverwrite=True. Export writes a linked World environment image as an MJCF<texture type="envhdr">; empty mesh assets (zero vertices or triangles) are skipped and do not produce mesh or geom references.forge_save_blendfile(filepath=None, overwrite=False, save_copy=False)saves the current blend or a named destination. Existing files—including the current blend path—require explicitoverwrite=True;save_copy=Truedoes not change the active blend path.forge_coacd(name, ...)wrapsbpy.ops.forge.coacdsfor one selectedVISUALmesh. It reads triangle faces from the evaluated mesh and creates collider hulls while preserving the source object; generated hull names are returned increated. Important parameters:threshold,max_convex_hull,preprocess_mode, sampling/MCTS controls,merge,decimate/max_ch_vertex,extrude/extrude_margin, andseed.
Create tips:¶
- Joints:
- Joint in forge is Z-UP based and use local +Z in parent space;
- Rotate the joint object so Z points along the desired rotation axis. Position the joint at the pivot (e.g. top of a pendulum), not at the body center.
- First consider the location is at link local space location
[0, 0, 0], then consider location by needing.
- Inertials:
create_inertialdefaults toinertial_model_type='VISUAL'andcheck_mesh_valid=False. The operator can also useCOLLIDER; enablecheck_mesh_valid=Truefor validation before calculation. The dedicated MCP wrapper uses the defaults. Primitive shapes use analytic mass and inertia; mesh geometry requires TetGen. When validation is enabled, boundary-only contact and separating coplanar faces are valid; actual crossings and non-separating coplanar overlap fail. An epsilon gap is not required for valid contact. - Names: operators use type prefixes for object names. If a name already exists, Blender automatically appends a numeric suffix such as
.001or.002to keep it unique. Read the actual names from the tool'screatedresult instead of guessing them, then useforge_batch_renameif needed. - Spacing: geometry sizes given at creation are full dimensions (BOX) / true radius & height (SPHERE / CYLINDER / CAPSULE), exported to mjcf half-extents automatically; all in parent-local meters.
- Collider: create collider for shape geometry is not necessary, if really necessary need set visual shape geometry
obj.Forge_Geometrypropscontypeandconaffinityto0.
Model create rules:¶
link:¶
Create use bpy.ops.forge.create_link() optionally based on selected existing object(s):
- With no selection, the default location is the 3D cursor. Otherwise, placement depends on the selected object's type.
- Select object in type
[LINK, FRAME, REPLICATE]location will at local space[0, 0, 0].- Select object in type
[VISUAL, COLLIDER]whose parent is[LINK, FRAME, REPLICATE]: new link parented under that parent, location at selected object's local position and rotation zeroed.- Select object in type
[VISUAL, COLLIDER]without parent in[LINK, FRAME, REPLICATE]: location at selected object's location, marked asRoot Link.- With no selection, the link is created in the
Worldcollection as a root candidate. Usebpy.ops.forge.mark_root_link()to mark it. Export also discovers parentless roots in theLinkandFramecollections; those roots are not exported twice.
inertial:¶
Use bpy.ops.forge.create_inertial(inertial_model_type='VISUAL', check_mesh_valid=False) with selected existing link objects.
- Compute mass, center of mass, and principal inertia from children of the chosen type (
VISUALorCOLLIDER), recursively throughFRAMEdescendants. ChildLINKhierarchies are excluded; at least one matching geometry object is required.- Geometry validation is optional and off by default. Mesh geometry requires TetGen; primitive shapes do not.
- Create one new
INERTIALchild per successful Link calculation. Existing inertials are not replaced.
joint:¶
Create use bpy.ops.forge.create_joint(joint_type='<ENUM>') based on selected existing link object.
- Parameter
joint_typein[FREE, BALL, SLIDE, HINGE], default isHINGE.
geometry shape:¶
visual: usebpy.ops.forge.create_geometry(geometry_type='<ENUM>', size_box=(1, 1, 1), size_radius=0.5, size_height=1)
- Select object in type
[LINK, FRAME, REPLICATE]: create one geometry per selected object as child of it, location at local space[0, 0, 0].- Select none objects: create at 3d cursor location.
- Parameter
geometry_typein[BOX, CAPSULE, CYLINDER, SPHERE]- Parameter used corresponding to
geometry_type:
BOX:size_boxCAPSULE:size_radius,size_heightCYLINDER:size_radius,size_heightSPHERE:size_radius- Size unit is
meter
collider: usebpy.ops.forge.create_collider(collider_type='<ENUM>', create_method='<ENUM>', auto_size=False, use_min_as_height=False, size_box=(1, 1, 1), size_radius=0.5, size_height=1)
Method of
create_methodisGEOMETRY_TYPEorCOLLIDER_TYPE:
GEOMETRY_TYPE: only for selectedvisualobject(s), will auto detect shape from selected visual, other parameter not need for this method —collider_typeis ignored, the collider takes the visual's own geometry type. For a mesh visual, preferCOLLIDER_TYPEwith an explicit shape collider whenever that shape meets the collision requirements; useGEOMETRY_TYPEonly when its resulting collider type follows the collider selection rule above.COLLIDER_TYPE: will need explicit choosecollider_type, this is the only method when select none or selectlink/frameobject.- Parameter
collider_typein[BOX, CAPSULE, CYLINDER, SPHERE, MESH, SDF]- Parameter used corresponding to
collider_typewhencreate_method='COLLIDER_TYPE':
BOX:size_boxorauto_sizeCAPSULE:size_radius,size_heightorauto_sizeCYLINDER:size_radius,size_heightorauto_sizeSPHERE:size_radiusorauto_sizeMESH: no other parameter need, only available whenvisualobject(s) selected, bake visual geometry to convex mesh colliderSDF: only with selected visuals; shares the visual's Mesh Data as a linked duplicate, preserving its parent and transform. Vertex edits affect both objects.auto_size: auto-detect the selectedvisualgeometry size; off by default. With alink/frameselection or no selection, explicitsize_*values (or defaults) are used instead. Areplicateselection is not accepted by this operator.use_min_as_height:CAPSULE/CYLINDERonly, default is off. When on, the height is taken from the selectedvisual's smallest bounding-box axis instead of its largest, and the shape is oriented along that axis. Only takes effect together withauto_size=True— with explicitsize_radius/size_heightthe values are used as given and the flag is ignored. The MCP toolforge_create_colliderexposes this asmin_is_height.- Size unit is
meter
geometry mesh:¶
Create with traditional method. Use bpy.ops.forge.define_model_type(Model_Type='<ENUM>') to set object to corresponding model type.
Model_Typeis corresponding to:VISUALfor visual object,COLLIDERfor collider object.
hfield:¶
Create use bpy.ops.forge.create_height_field():
- Only can create when nothing selected, exist as root node without parent.
- Default location at 3d cursor location.
plane:¶
Create use bpy.ops.forge.create_plane():
- Only can create when nothing selected, exist as root node without parent.
- Default location at 3d cursor location.
site:¶
Create use bpy.ops.forge.create_site(site_type='<ENUM>', size_box=(0.2, 0.2, 0.2), size_radius=0.1, size_height=0.2) based on selected existing link object(s).
- Parameter
site_typein[BOX, CAPSULE, CYLINDER, SPHERE]- Parameter used corresponding to
site_type:BOX:size_boxCAPSULE:size_radius,size_heightCYLINDER:size_radius,size_heightSPHERE:size_radius- Size unit is
meter
actuator:¶
Create use bpy.ops.forge.create_actuator(actuator_type='<ENUM>') based on selected existing link | joint | tendon object(s).
- Parameter
actuator_typein[GENERAL, MOTOR, POSITION, VELOCITY, ADHESION]
tendon:¶
Create use bpy.ops.forge.create_tendon(tendon_type='<ENUM>') based on selected existing joint object(s).
- Need select at least 2
jointobjects, create singletendonfrom ordered selection.- Only support
jointsubtype in[SLIDE, HINGE].- Parameter
tendon_typecurrently onlyFIXED.- Every ordered target item contains both
targetand exportedcoef(default0.5). Useforge_set_tendon_targets(name, [{"target": "JNT_A", "coef": 0.25}, ...])to replace the path without raw collection scripting.
equality:¶
Create use bpy.ops.forge.create_equality(equality_type='<ENUM>') based on exactly 2 selected existing link | joint | site objects.
- Selected
jointobjects →equality_typeisJOINT.- Selected
link/siteobjects →equality_typeisCONNECTorWELD.
contact:¶
Create use bpy.ops.forge.create_contact(contact_type='<ENUM>') based on selected existing link | visual | collider | plane | hfield objects.
- Need select at least 2 objects.
- Selected
linkobjects →contact_typeisEXCLUDE.- Selected
visual/collider/plane/hfieldobjects →contact_typeisPAIR.- Create one
contactfor each pair combination of selected objects.
sensor:¶
Create use bpy.ops.forge.create_sensor(Sensor_Type='<ENUM>', set_ref=False, set_xbody=False, set_subtree=False) based on selected existing link | joint | site | visual | collider | camera objects.
Sensor_Typeavailable values are decided by selected target model type:
joint:Jointpos,Jointvelsite:Touch,Accelerometer,Velocimeter,Gyro,Framepos,Framequat,Framexaxis,Frameyaxis,Framezaxis,Framelinvel,Frameangvel,Contactlink:Framepos,Framequat,Framexaxis,Frameyaxis,Framezaxis,Framelinvel,Frameangvel,Contact,Subtreecom,Subtreelinvel,Subtreeangmomlinkwithset_xbody=True:Framepos,Framequat,Framexaxis,Frameyaxis,Framezaxis,Framelinvel,Frameangvellinkwithset_subtree=True:Contactvisual/collider:Framepos,Framequat,Framexaxis,Frameyaxis,Framezaxis,Framelinvel,Frameangvel,Contactcamera:Framepos,Framequat,Framexaxis,Frameyaxis,Framezaxis,Framelinvel,Frameangvel- mixed selection in
[LINK, SITE, VISUAL, COLLIDER, CAMERA](e.g. 2 objects with different types):Framepos,Framequat,Framexaxis,Frameyaxis,Framezaxis,Framelinvel,Frameangvel,Contactset_ref=True: need select 2 objects, first selected is target, second is reference;Sensor_Typerange asFramepos~Contact; withsitetarget start fromTouch; withlinktarget andset_xbody=Trueend atFrameangvel.set_xbody/set_subtreeonly forlinktarget, mutual exclusive.- Contact subtree sensors must explicitly set both
subtree1andsubtree2. Select exactly two existinglinkobjects in order[subtree1_root, subtree2_root], then callforge_create_sensor(sensor_type="Contact", set_ref=True, set_subtree=True, select=[subtree1_root, subtree2_root]). The first root becomesForge_Sensor.target(MJCFsubtree1), the second becomesForge_Sensor.ref(MJCFsubtree2). Do not create a one-sided subtree sensor or leaverefunset. Verify both pointers,use_subtree=Trueandtarget_type="SUBTREE"; after export, verify the<contact>sensor contains both attributes with the intended body names.
light:¶
Create use bpy.ops.forge.create_light(light_type='<ENUM>', light_mode='<ENUM>') optionally based on selected existing link object(s).
- Select none objects create world light at 3d cursor location,
light_typein[DIRECTIONAL, SPOT, POINT],light_modein[FIXED, TRACK, TRACKCOM].- Select 1
link:light_typein[SPOT, POINT],light_modein[TARGETBODY, TARGETBODYCOM, FIXED, TRACK, TRACKCOM]; whenlight_modeis[TARGETBODY, TARGETBODYCOM]create single world light at 3d cursor location targeting the selectedlink, other modes create child light of it.- Select 2
links: whenlight_modeis[TARGETBODY, TARGETBODYCOM]create single light, first selected is light position, second is target; other modes create one light per selectedlinkas child of it.- Select 3+
links:light_moderestricted to[FIXED, TRACK, TRACKCOM], create one light per selectedlinkas child of it.
camera:¶
Create use bpy.ops.forge.create_camera(camera_type='<ENUM>', camera_mode='<ENUM>') optionally based on selected existing link object(s).
camera_typein[PERSPECTIVE, ORTHOGRAPHIC].- Select none objects create world camera at 3d cursor location,
camera_modein[FIXED, TRACK, TRACKCOM].- Select 1
link:camera_modein[TARGETBODY, TARGETBODYCOM, FIXED, TRACK, TRACKCOM]; whencamera_modeis[TARGETBODY, TARGETBODYCOM]create single world camera at 3d cursor location targeting the selectedlink, other modes create child camera of it.- Select 2
links: whencamera_modeis[TARGETBODY, TARGETBODYCOM]create single camera, first selected is camera position, second is target; other modes create one camera per selectedlinkas child of it.- Select 3+
links:camera_moderestricted to[FIXED, TRACK, TRACKCOM], create one camera per selectedlinkas child of it.
frame:¶
Create use bpy.ops.forge.create_frame() optionally based on selected existing link | frame | replicate object(s).
- Select none objects create at 3d cursor location.
- Select objects create one frame per selected object as child of it.
- Shift + Click: reverse parent, selected objects become child of new frame.
replicate:¶
Create use bpy.ops.forge.create_replicate(count=0, sep='', offset=(0, 0, 0), euler=(0, 0, 0)) based on selected existing camera | light | visual | collider | site | link | frame | attach | replicate objects.
- Parameter
count: replicate count.- Parameter
sep: separator string.- Parameter
offset: offset vector between copies.- Parameter
euler: euler rotation between copies.- Create single
replicateobject, selected objects as its source.- A
replicateowns its source: the selected objects move into the replicate's own viewport-hidden collection and cannot be selected afterwards. Replicate objects that no later operation still needs to select.
attach:¶
Create use bpy.ops.forge.create_attach() optionally based on selected existing link | frame | replicate object.
- Need select exactly 1 object or none.
- Choose file in dialog via
Forge_Temp_Props:tmp_file,tmp_model,tmp_prefix,tmp_link.- Import the model from external mjcf file into scene, then create attach reference.
- Requires
Forge Libraryasset library (Preferences -> File Paths -> Asset Libraries).- The file must not be one already
included in the scene — attach and include take separate files. Sharing one reportsAttach fail: list index out of range.
include:¶
Create use bpy.ops.forge.create_include() only when nothing selected.
- Choose file in dialog via
Forge_Temp_Props.tmp_include.- Only add
includetag when export, content of the file will not be exported.
Material creation rules:¶
Materials are PBR datablocks from the bundled Forge library — not mjcf model objects; they attach to visual, hfield or plane geometry.
material:¶
Use MCP forge_create_material(category, material, use_existing=True, select=[...]), which wraps bpy.ops.forge.create_material(category='<str>', material='<str>'), based on selected visual, hfield or plane object(s):
- Requires a non-empty selection where every object is type
VISUAL,HFIELDorPLANE— poll fails when nothing is selected or any other type is selected.- Parameter
categoryin[Crystal, Human, Liquid, Manmade, Metal, Organic, Plastic](~110 materials total).Parameter
materialis the exact library name inside the category — e.g.Gold,Water,Skin I,Titanium (Anodized); names carry parenthesized variants, match them exactly. Enumerate at runtime:Builds a Principled BSDF material datablock named after the library entry (base color is linear, set directly), non-render fields (
density,viscosity, ...) stored as custom ID properties.- Parameter
use_existing(defaultTrue): when an existing datablock with the same name is present it is assigned as-is — user tweaks are preserved, it is NOT rebuilt from the library. Passuse_existing=Falseto always build a fresh datablock from the library (Blender suffixes the taken name.001).- Assigns to the first object-level material slot of every selected object (creating the slot when empty). This applies equally to mesh and primitive
visual,hfield, andplaneobjects; shape/material modifier sockets are not edited by the operator.
Enumerate categories / names from Python running inside Blender:
import bpy
import importlib
# Resolve both legacy and extension-managed installs without assuming the
# extension repository is named vscode_development or user_default.
module_name = next(
(name for name in bpy.context.preferences.addons.keys() if name == "Forge" or name.endswith(".Forge")),
None,
)
if module_name is None:
raise RuntimeError("Enable the Forge add-on before inspecting its material library")
materials = importlib.import_module(f"{module_name}.core.materials")
materials.get_categories() # ['Crystal', 'Human', 'Liquid', ...]
materials.get_material_names("Metal") # ['Aluminum', 'Brass', 'Chromium', ...]