Forge modify¶
Modify MJCF model elements with Forge tools.
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. Modify the model in the scene namedSceneonly — do not rename or delete it, and do not modify 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 dedicated MCP tools: inspect with
forge_blendfile_summary,forge_scene_info,forge_object_infoandforge_measure; change model/type/parent withforge_define_*; edit values withforge_set_transform,forge_set_shape_size,forge_set_propertyandforge_rename; validate withforge_mesh_check. Useforge_executeonly for a Forge property or collection edit that no dedicated tool can express. Its skill check applies to explicitbpy.ops.forge.*calls and directForge_*property writes; ordinary Blender modelling edits are allowed even when the target object has a Forge model type. - Skill gate scope: generic Blender deletion/removal operations remain available without a Forge guide. Use the dedicated Forge operations for Forge hierarchy changes, especially
forge_define_parent; directobj.parentassignment is only the documented detach escape hatch below. - 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 → edit → verify → validate → save/export. Read object types, parents, locks and current property values before mutation. Re-read every changed object, remeasure world-space bounds, mesh-check the relevant visuals, and use
forge_screenshotwhen visual confirmation adds value. - Every MCP
select=[...]argument is ordered. The first name becomes the active object and list indices become Forgeselected_ordervalues. Forforge_define_parent, put the intended parent first and the children after it; for target/reference operations preserve the order documented in the create rules. forge_object_inforeturns the wholeForge_<Type>group, modifier sockets and transforms — but not Blender-nativeobj.data. Read light / camera data (energy,angle,clip_start, ...) withforge_execute; write it withforge_set_propertydata:-prefixed keys.forge_deleterecursively removes all descendants of every named root. Inspectchildrenfirst and name only the intended roots. Inspect output destinations before save/export and do not overwrite files unless requested.
Modify tips¶
- Joint axis: the exported mjcf
axisis the joint object's local +Z expressed in parent space (obj.matrix_local.to_3x3().transposed()[2]). Default creation gives +Z along parent +Z; rotate the joint object's euler so its Z aligns with the desired world axis (e.g.rotation_euler = (-90°, 0, 0)makes the axis point along parent +Y). - Local vs world coordinates:
locationand shape sizes are in the parent's local space. When a link sits below / away from the world origin, convert:local = world - parent_world(for unrotated parents). Verify placements with evaluated world bounding boxes, not socket values. - Shape size sockets are arrays for BOX (
Socket_0= full xyz dimensions, written as a 3-tuple) and scalars for SPHERE (visualSocket_0/ colliderSocket_3= radius); assigning a scalar to an array socket raisesTypeError. On Blender 5.2+ read/write a socket asmod.properties.inputs.Socket_N.value; on Blender 4.x–5.1 usemod["Socket_N"]. Preferforge_set_shape_size, which handles both. Modifier names capitalize the shape (Geometry_Box,Collider_Sphere).forge_set_shape_sizecovers all four shapes: BOX takes the full (x, y, z) tuple, SPHERE a radius, CAPSULE / CYLINDER a(radius, height)pair — height is the full length of the straight section (capsule overall length = height + 2·radius); height socket is visualSocket_2, collider / siteSocket_4. - mesh_check scope: the check reports pairwise intersections between all selected objects. Boundary-only point-point, point-edge, point-face, edge-edge, and edge-face contact is allowed when incident geometry does not cross the other mesh. A coplanar shared face is also allowed when its plane separates the two complete objects (for example aligned boxes placed flush), independent of face winding. Non-separating coplanar area overlap and actual face crossings still count as intersections. Scope selection per-link to avoid reporting unrelated geometry.
- Name collisions: Blender silently suffixes duplicates (
.001) — including against orphaned datablocks left out of the scene after failed operations. Renaming to a "free-looking" name can no-op; check the result or purge orphans first. - Locked transforms: Forge locks scale on most model objects (and location/rotation on some); scripts must respect
lock_location/lock_rotation/lock_scaleinstead of assigning. forge_set_propertyvalue types are not what the names suggest — always read the returnedskippedmap. A write that lands inskippeddid not happen, and the tool still returns success. Two traps seen in practice: severalForge_*flags are uppercase string enums, not bools (actuatorctrllimitedtakes'FALSE'/'TRUE'/'AUTO'— passingtrueor"true"is skipped), and array properties have fixed arity (actuatorgearneeds 6 elements, not 1). Theskippedvalue carries the underlyingbpy_structmessage, which names the expected type or arity.
Modify rules¶
Modify order:¶
- Select target object(s).
- Change the type with the appropriate documented
bpy.ops.forge.define_*operator when needed. - Edit parameters supported by the model type through
obj.Forge_<Type>.<property>directly; popup dialogs are UI-only. - Adjust
location,rotation, andscaleas needed, respecting locked axes.
Relationship retargeting:¶
Post-creation relationship changes go through forge_set_property Object-target properties — pass the target object's name as a string, null clears:
actuator.target— anylink/joint/tendonobject;target_typere-detects from the new target.equality.target1/target2—JOINTequality usestarget1and an optionaltarget2; when present, both are exported as joint references.contact.target1/target2.sensor.targetandsensor.ref(frame sensor types and Contact filters). For a Contact subtree sensor, both must reference existinglinkobjects:targetexports assubtree1,refassubtree2. Set both explicitly when creating or retargeting; do not clear either endpoint. Verifyuse_subtree=True,target_type="SUBTREE"and both pointers after editing, and both named attributes in the exported<contact>sensor.light.target/camera.target— only meaningful whilemodeisTARGETBODY/TARGETBODYCOM.- Tendon joint path:
forge_set_tendon_targets(ordered replace).attachreference fields (model/file/link/prefix) are plain strings, not pointers. - Detach has no dedicated implementation:
forge_define_parentonly assigns a parent; no operator or tool clears one. The only path is theforge_executeescape hatch (obj.parent = None; collection membership is not updated) — otherwise report the gap.
Model modify rules:¶
model type:¶
bpy.ops.forge.define_model_type(Model_Type='<type>') only for selected UNDEFINED type object(s):
Available
Model_Typedecided by selected object type:
EMPTYobject:LINKMESHobject:VISUALorCOLLIDERLIGHTobject:LIGHTCAMERAobject:CAMERA
VISUAL: set geometry typeMESH, group2,contype/conaffinity0, rename with prefixGEO_.COLLIDER: set geometry typeMESH, group3, rename with prefixCOL_.LINK: rename with prefixLNK_.LIGHT: set light type by Blender light type (SUN→DIRECTIONAL, elseSPOT/POINT), modeFIXED, rename with prefixLIT_;AREAlight not support.CAMERA: set camera type by Blender camera type (ORTHO→ORTHOGRAPHIC,PERSP→PERSPECTIVE), modeFIXED, rename with prefixCAM_; onlyPERSP/ORTHOcamera support.- Object with parent move to corresponding type collection, without parent move to
WORLDcollection.
geometry type (visual):¶
bpy.ops.forge.define_geometry_type(geometry_type='<type>', apply_to_all=False) based on selected visual object(s):
- parameter
geometry_typein[BOX, CAPSULE, CYLINDER, SPHERE, MESH, SDF]apply_to_all: defaultFalseonly apply to active object,Trueapply to all selected.- Size is preserved as the bounding-box equivalent; exact shape parameters may differ across a round-trip through another shape (e.g. CYLINDER r=0.05 -> SPHERE -> CYLINDER yields r=0.1).
- Switch to
MESH/SDF: bake primitive geometry into mesh, scale unlock. Switching between MESH and SDF preserves the mesh and modifiers. SDF exports with a mesh asset reference.- Switch from
MESH/SDF: use object dimensions as size.
collider type:¶
bpy.ops.forge.define_collider_type(collider_type='<type>', apply_to_all=False) based on selected collider object(s):
- parameter
collider_typein[BOX, CAPSULE, CYLINDER, SPHERE, MESH, SDF]- Size is preserved as the bounding-box equivalent; exact shape parameters may differ across a round-trip through another shape (e.g. CYLINDER r=0.05 -> SPHERE -> CYLINDER yields r=0.1).
- Switch to
MESH/SDF: bake current primitive or convex modifier into mesh, scale unlock. Existing mesh geometry is preserved.- Switch from
MESH/SDF: use object dimensions as size.
site type:¶
bpy.ops.forge.define_site_type(site_type='<type>', apply_to_all=False) based on selected site object(s):
- parameter
site_typein[BOX, CAPSULE, CYLINDER, SPHERE]- Size is preserved as the bounding-box equivalent; exact shape parameters may differ across a round-trip through another shape.
joint type:¶
bpy.ops.forge.define_joint_type(joint_type='<type>', apply_to_all=False) based on selected joint object(s):
- parameter
joint_typein[FREE, BALL, SLIDE, HINGE]- Update joint shape gizmo when type changed.
actuator type:¶
bpy.ops.forge.define_actuator_type(actuator_type='<type>', apply_to_all=False) based on selected actuator object(s):
- parameter
actuator_typein[GENERAL, MOTOR, POSITION, VELOCITY, ADHESION]
parent:¶
bpy.ops.forge.define_parent() change parent of selected object(s) to active object:
- Active object is
link|frame|replicate: parent selected object(s) in[LINK, VISUAL, COLLIDER, JOINT, SITE, LIGHT, CAMERA, FRAME, REPLICATE, ATTACH]to it.- Active object is
visual: refused (poll fails) — acolliderunder avisualis not exported; parent thecolliderto thelinkinstead.- Children of a selected
visualobject (for example colliders still bound to it) are reparented to the active object first, so a legacycollider-under-visualnesting is repaired rather than recreated.- World position of child preserved after reparent.
root link:¶
bpy.ops.forge.mark_root_link() based on selected link | frame | replicate object(s):
- Parentless objects are marked directly.
- Objects with a parent are unparented while preserving their world transform, then marked.
- Set
obj["Root Link"] = True.
mesh check:¶
bpy.ops.forge.mesh_check() based on selected visual object(s):
- Check mesh validation, report error info.
- Use before compute inertial / export.
gizmo size:¶
bpy.ops.forge.scale_gizmo_size() change gizmo display size of selected object(s), or all objects when none selected:
- Scale from scene
Forge_Props.gizmo_scale.
Utilities:¶
batch rename:¶
bpy.ops.forge.batch_rename(new_name='', numerate='###') based on selected object(s):
- Rename selected objects to
{new_name}_{index}with zero padding bynumeratelength, index start from 0.
instance idname:¶
bpy.ops.forge.set_instance_idname() based on selected link object(s):
- Mark selected links as instances of scene
Forge_Props.instance_body(INSTANCE_IDNAMEcustom property); run again with no instance body to clear. Links already markedINSTANCE_LINKare refused (warning).- Mark the instance source first with
bpy.ops.forge.mark_instance_link()(INSTANCE_LINKcustom property, toggle). A marked link never exports as a<body>; referenced ones provide assets for their copies, unreferenced ones export nothing at all (warned);Forge_Props.instance_bodyonly lists marked links. The instance link's children are written into everyINSTANCE_IDNAMElink, element names prefixed<instance>_<name>; global sections (actuator/sensor/ …) are not duplicated per instance.
select by tag:¶
bpy.ops.forge.select_by_tag() only in object mode:
- Select objects whose name contains scene
Forge_Props.select_tag.
select hulls:¶
bpy.ops.forge.select_hulls() only in object mode:
- Select convex hull objects by name template from addon preferences (
coacd_name_template).
Parameter modify rules:¶
Each model type has its own parameter group obj.Forge_<Type> on the object — always change parameters by direct property assignment. bpy.ops.forge.properties_* popup dialogs are UI-only and ineffective for agents; never use them.
Direct property access:¶
Parameters grouped by model type, then by sub-type (set property type / mode directly to switch sub-type, same as bpy.ops.forge.define_*). Shape / gizmo sizes live in geometry-node modifier sockets obj.modifiers[...] — readable / writable as mod.properties.inputs.Socket_N.value on Blender 5.2+ and mod["Socket_N"] on Blender 4.x–5.1. Both surfaces are Forge edits, so forge_set_property and forge_set_shape_size ask for this guide.
link:obj.Forge_Link—mocap,gravcompvisual/collider/hfield/plane(all mjcfgeom):obj.Forge_Geometry—group,condim,contype,conaffinity,priority,friction,mass,density,solmix,solref,solimp,margin,gap(hfield / plane UI hidesfriction/mass/density, but they are exported when non-default)- shape size in geometry modifier
obj.modifiers[...]:visual:BOXsizeSocket_0;SPHEREsizeSocket_0;CAPSULE/CYLINDERsizeSocket_0+ heightSocket_2;MESHno size sockets. Material is the first object-level material slot, assigned byforge_create_material.collider:BOX/SPHEREsizeSocket_3;CAPSULE/CYLINDERsizeSocket_3+ heightSocket_4;MESHno size socketshfield:nrowSocket_0,ncolSocket_2,use_elevationSocket_3,radius_xSocket_4,radius_ySocket_5,elevation_zSocket_6,base_zSocket_7,height_mapSocket_8,use_height_mapSocket_10,offsetSocket_11; material is the first object-level material slot.plane:half_size_xSocket_2,half_size_ySocket_3,infiniteSocket_4; material is the first object-level material slot.
- shape size in geometry modifier
site:obj.Forge_Geometry—group; shape size in site modifier:BOX/SPHEREsizeSocket_3;CAPSULE/CYLINDERsizeSocket_3+ heightSocket_4joint:obj.Forge_Jointper type:rangeonBALL/HINGEis stored in radians. Export converts it to degrees only whenForge_GlobalConfig.g_compiler.angle == "DEGREE"; with"RADIAN"it is written unchanged. For a HINGE limit of ±90°, setrange = [-pi/2, pi/2], not[-90, 90].forge_object_inforeturns the stored radians.refandspringrefonHINGEfollow the same rule, and are scalars: a 90° reference isref = pi/2, exported asref="90". ASLIDE'srange,refandspringrefare linear values (meters) and are written unchanged whatever the compiler unit;actuatorfrcrangeis a force range, not an angle. ABALLhas noreforspringref: MJCF's ball joint does not carry them, so they are not exported at all.FREE:stiffness,armature,damping,frictionlossBALL:springdamper,solreflimit,solimplimit,solreffriction,solimpfriction,stiffness,range,limited,margin,armature,damping,frictionlossSLIDE/HINGE: BALL params +ref,springref,actuatorfrcrange,actuatorfrclimited- gizmo modifier:
display_sizeSocket_0,rangedisplaySocket_3
actuator:obj.Forge_Actuator—type,target,target_type(auto-detected fromtarget) always; per type:GENERAL/MOTOR:ctrllimited,ctrlrange,forcelimited,forcerange,gear,gaintype,biastype,dynprm,gainprm,biasprmPOSITION: same asGENERAL+kp,kv,inheritrangeVELOCITY: same asGENERAL+kvADHESION:ctrllimited,ctrlrange,forcelimited,forcerange,gain(nogear/dynprm/gainprm/biasprm)
tendon:obj.Forge_Tendon—type,targets(ordered target path; everytargets.items[]hastargetand exportedcoef, default0.5; replace safely withforge_set_tendon_targets, or edit withforge.add_tendon_target_item/forge.remove_tendon_target_item),range,limited,solreflimit,solimplimit,springlength,stiffness,dampingequality:obj.Forge_Equality—active,type,target1,target2,target_type;target2is optional, including forJOINTequality. Type-specific parameters:CONNECT:anchor,solref,solimpWELD:anchor,solref,solimp,relposeJOINT:solref,solimp,polycoef
contact:obj.Forge_Contact—type,target1,target2always; per type:EXCLUDE: no extra paramsPAIR:condim,friction,solref,solreffriction,margin,gap
sensor:obj.Forge_Sensor—type,target_type,target,noise,cutoffalways; per type:Framepos/Framequat/Framexaxis/Frameyaxis/Framezaxis/Framelinvel/Frameangvel:ref_type,ref,use_xbody.ref_typeis independent oftarget_typeand exports as MJCFreftype; use the actual reference object's kind — MJCFreftypeaccepts onlyBODY,XBODY,GEOM,SITE,CAMERA, so aJOINTcan never be a reference (create_sensorrefuses a selection that pairs a joint with another object).Contact:ref,use_subtree,num(auto-computed fromdata),data,reduce. Withuse_subtree=True, explicitly set bothtarget(subtree1) andref(subtree2) tolinkobjects; a missing second root is incomplete under this authoring workflow.Touch/Accelerometer/Velocimeter/Gyro/Jointpos/Jointvel/Subtreecom/Subtreelinvel/Subtreeangmom: no extra params
light:obj.Forge_Light—type,mode;targetonly whenmodeisTARGETBODY/TARGETBODYCOM; Blender light dataobj.data:use_shadow,color,energy;SPOT/POINTadditionallyshadow_soft_size,cutoff_distance;DIRECTIONALinsteadangle;SPOTadditionallyspot_size,spot_blendcamera:obj.Forge_Camera—type,mode;targetonly whenmodeisTARGETBODY/TARGETBODYCOM; Blender camera dataobj.data:PERSPECTIVEangle/ORTHOGRAPHICortho_scale, plusclip_start,clip_endinertial:obj.Forge_Inertial—mass,diaginertia,fullinertia(position / quaternion from object transform)attach:obj.Forge_Attach—model,file,link,prefixreplicate:obj.Forge_Replicate—count,sep,offset,euler(count/offset/eulermirrored to modifier socketsSocket_0/Socket_2/Socket_3)include:obj.Forge_Include—fileis the user-facing/reference filename;rawfileis the resolved source path used by export. Preserve or updaterawfilewhen relocating an include, otherwise export may read the stale source.
Scene-level configs — Forge_GlobalConfig / Forge_VisualConfig:¶
File workflow, compiler / option / statistic and visual settings live on the scene, not on objects. Use forge_scene_config(group) to inspect and forge_set_scene_property(group, values) to edit. The direct equivalents are scene.Forge_Props, scene.Forge_GlobalConfig.<group> and scene.Forge_VisualConfig.<group> (same rule as object params: popup operators are UI-only). Every global/visual group and sub-group has a reset() method; only non-default properties are exported.
-
Forge_Props(group="forge_props") — workflow/display settings:scale_factor,gizmo_scale,rel_path,select_tag,import_file_path,export_path,import_material,update_forge_only,instance_body,world_collection,use_glb. Treatis_importing,is_exporting,is_attaching,is_including, the modal-operation slot (modal_operation_active,modal_operation_name,modal_operation_token,modal_operation_deferred,modal_operation_status,modal_operation_message,cancel_requested), progress values and info strings as operation state; read them but do not manually drive them. Only one modal Forge operation may run at a time — a second one fails whilemodal_operation_activeis set. -
Forge_GlobalConfig— exported as root-level mjcf sections:g_compiler(<compiler>):autolimits,anglein[RADIAN, DEGREE],eulerseqin[XYZ, XZY, YXZ, YZX, ZXY, ZYX],assetdir,meshdir,texturedirg_option(<option>):timestep,iterations,tolerance,gravity(float[3]),o_solref/o_solimp/o_friction(float arrays, contact override),o_marging_option.flag(<flag>):constraint,equality,frictionloss,limit,contact,gravity,actuation,refsafe,override— bool, exported as enable / disable
g_statistic(<statistic>):extent,center(float[3])
Forge_VisualConfig— exported as the<visual>section:v_global(<global>):orthographic,fovy,azimuth,elevation— angle props are radians (converted to degrees at export)v_headlight(<headlight>):active(0/1),ambient/diffuse/specular(float[3] color)v_map(<map>):fogstart,fogend,znear,zfarv_rgba(<rgba>):fog,haze,joint,bv,contactforce(float[4] color)v_quality(<quality>):shadowsize,pointshadowsizev_tonemapping(<tonemapping>):methodin[none, aces]v_ssgi(<ssgi>) (Motphys extension):active,resolutionscale,raycount,stepcount,thickness,intensity,gidenoiseoffset,rraycount,rdenoiseoffset
Popup dialogs — ineffective for agents:¶
All bpy.ops.forge.properties_* popups (properties_link, properties_shape, properties_shape_site, properties_hfield, properties_plane, properties_joint, properties_actuator, properties_tendon, properties_equality, properties_contact, properties_sensor, properties_light, properties_camera, properties_replicate, and scene-level properties_global / properties_visual) open a modal UI window that cannot be driven programmatically — never use them. Every parameter they edit is reachable via the direct property access above.
The file-browser helpers are dialog-driven too — assign their target property directly instead:
forge.temp_file/forge.temp_link/forge.temp_include→ writescene.Forge_Temp_Props.tmp_file/tmp_model/tmp_prefix/tmp_link/tmp_includedirectly (this is whatforge_create_attach/forge_create_includedo).forge.file_path/forge.export_path→ writescene.Forge_Props.import_file_path/export_path(plusrel_path) directly (this is whatforge_import_mjcf/forge_export_mjcfdo).