Skip to content

Changelog: v2.10 ​

v2.10.3 (2026-10-25) ​

Rebuild applications after updating (raisim::World ABI change).

RaiSim

  • Build: the Linux x86-64 libraries are built for the x86-64-v3 instruction set (AVX2, FMA, BMI1 and BMI2, F16C, LZCNT, MOVBE) instead of the CPU of the machine that built them (-march=native), so which CPUs a release runs on no longer depends on that machine. See Introduction.
  • Behaviour change: articulated systems integrate with IntegrationScheme::SEMI_IMPLICIT by default (it was TRAPEZOID): the positions move with the step's new velocity, the one the contact solver leaves, so an impact that stops a body stops it at the surface instead of half a step's travel in. Against analytic or converged solutions it was as accurate or more accurate wherever the forces depend on the state. At a 1 ms step, an undamped pendulum was off by 2 mrad after 10 s (126 mrad with TRAPEZOID, whose swing grew 9% in 20 s), a box tumbling about its middle axis by 0.007 mrad after 4 s (471 mrad), a falling 10-link chain by 62 mrad after 1 s (209 mrad), and a PD-controlled arm tracking a sine by 1.0 mrad (3.3 mrad). TRAPEZOID stays exact under a constant force: a projectile is 4.9 mm low after 1 s of flight with SEMI_IMPLICIT (24.5 mm at a 5 ms step). A PD-controlled system that has been in contact now keeps SEMI_IMPLICIT; it was integrated with TRAPEZOID whatever its scheme. SEMI_IMPLICIT also takes the place TRAPEZOID took for EULER and RUNGE_KUTTA_4 there, and for RUNGE_KUTTA_4 on systems with loop or mimic constraints. Simulations, and policies trained in them, move differently. A system on the default scheme gets its previous motion back with setIntegrationScheme(IntegrationScheme::TRAPEZOID); a PD-controlled one that requested SEMI_IMPLICIT, EULER or RUNGE_KUTTA_4 moved with TRAPEZOID after its first contact, which no setting reproduces now (see Dynamics and Control). Benchmarks: the articulated ones changed by -11% to +2.5%, except atlas_ground (Atlas falling into a heap in torque mode), which went from 4.2 s to 22.5 s. Atlas lands in a different heap, with its legs crossed, where the contact solver runs 131 iterations a step instead of 9; either scheme continues that heap at the same cost, and over 25 starting poses the mean was 71 iterations with SEMI_IMPLICIT against 56 with TRAPEZOID.
  • Fix: swept CCD left a body that a speculative contact had stopped resting just above the surface, out of reach of the discrete detection, and a sweep from where it rested found nothing. Pushed down on a later step (a foot under its leg), it went into the ground by the step's travel: 9 mm for a standing ANYmal's feet at a 5 ms step. A body within sweptCcdSpeculativeMargin of the terrain keeps speculative contacts, at a box's corners and on a cylinder's rims as touching contacts would be. With swept CCD on, a standing ANYmal costs 1.3% more a step, and 128 boxes and cylinders resting on the ground 29% more: the solver works three iterations a step on them instead of one.
  • Fix: with sleeping enabled (the default), a moving kinematic body passed through sleeping bodies without waking them. A box asleep on a kinematic platform floated in place when the platform moved down and was passed through when it moved up, and a kinematic pusher swept through a sleeping box. A moving kinematic body (non-zero velocity, or a pose set since the last step) now wakes the sleeping islands its collision bodies reach between the last step and the end of this one, including what slept on a platform that setPosition() moved away, and keeps the islands it touches awake. See World.
  • Fix: a sleeping body switched to BodyType::KINEMATIC stayed asleep and ignored the velocity it was given; it now stops sleeping and moves.
  • Fix: restoring a World checkpoint shrank an articulated system's contact Jacobians to the sizes they had at the capture. A later contact on a body deeper in the tree than the captured one (a foot after a contact on the base, for example) then wrote past them and corrupted the heap. Assigning a SparseJacobian now keeps its storage, growing it to the source's capacity if needed; SparseJacobian::size starts at zero.
  • Fix: contacts between a convex mesh and a triangle mesh, between two triangle meshes, and between two convex meshes were placed at support points of the bodies along the contact normal. Where faces touch flat, those are arbitrary vertices of the faces, up to a metre outside both bodies. A default World::addMesh() box sank into a static triangle-mesh floor, a triangle-mesh box into a triangle-mesh plate, and convex meshes stacked or not depending on which was created first. Contacts are now placed at the EPA witness points, inside the overlap of the touching faces, and two convex meshes touching face to face get up to four contacts spread over the corners of their overlap instead of one, so they no longer tip about a single point (a disc, crossed bars and stacked boxes rest). contact::EPAResult has new witnessA and witnessB members, taken from the face of the polytope that holds its point nearest the origin. Piles of convex meshes carry somewhat more contacts: mesh_collider_plane takes 3% longer, and in mesh_collider_speed the dropped hulls now come to rest leaning on each other instead of being knocked apart, which makes its ground scene 2.2 times as long. g1_hand_self_collision keeps its mean step time, its 99th percentile goes from 100 to 115 µs. See Collision Detection and Colliders.
  • Fix: two convex meshes touching in a symmetric pose (equal boxes stacked aligned, bars crossed at a right angle) could get a horizontal contact normal with no depth: EPA oriented the face of its starting polytope that passes through the origin by the sign of a vanishing distance. Faces are now oriented away from a point inside the polytope. Such stacks used to sink, spin or be kicked by contacts as deep as the bodies are wide.
  • Fix: contacts of a box with a triangle mesh (MeshCollisionMode::ORIGINAL_MESH) came one per triangle, each measured as if the triangle were the half-space behind it. Where a triangle crossed the box, a contact was as deep as the box is wide, along a normal lying in a box face: a mesh box resting on a box was pushed sideways, a box on a mesh step slid off its edge, and meshes landing on a box or on a thin plate fell through it. One contact per triangle also left a box lying inside one large triangle of a floor rocking on a single contact 5 to 16 mm deep, and on floors of slivers or of 1 cm squares the contacts kept were all next to one corner, so most boxes dropped on them fell through. Each triangle is now tested against the box along the triangle's normal, the box's axes and the crosses of their edges, and the box leaves it the shortest way, except that it is held off along the surface's normal where that way would only slide it along a flat mesh surface, and along the triangle's own normal where a curved mesh presses into a box face at an angle. Faces against faces touch at the corners of their overlap, each at its own depth; a box's contacts keep every surface it lies against (the floor and both walls of a mesh bin's corner), spread over each. Boxes set down on mesh floors of slivers, of 1 cm squares or of two triangles, also with zero-area triangles that repeat a corner, now rest where they were set. Over 20000 poses of the G1 pelvis box against the hand mesh of mesh_primitive_proximity, 2147 of 2634 contacts were deeper than the bodies overlap, by up to 8.5 cm; none are now, and that case takes 75% longer (99th percentile 25 to 59 µs, worst call 51 to 150 µs). See Collision Detection and Colliders.
  • Fix: a robot keeps only the first contact of each pair of its own bodies by default (ArticulatedSystem::setAllowedNumberOfInternalContactsBetweenTwoBodies()), and a box's contacts with a mesh came in the order found, so the one kept was arbitrary, often a shallow one at the side of a mesh deep in the box. They now come deepest first. Together with the fix above, the hands of g1_hand_self_collision sink into the body's boxes at most 5.4 mm instead of 4.5 cm over 24 phases of its motion; its mean step time is unchanged and its 99th percentile drops from 116 to 72 µs.
  • Fix: MJCF contype and conaffinity were loaded as a RaiSim collision group and mask, which RaiSim tests both ways where MuJoCo needs either, and every geom with conaffinity="0" was dropped. Gymnasium's Ant, HalfCheetah and Walker2d had no collision bodies and fell through their floors. Geoms now pair up as in MuJoCo: two touch when the contype of either shares a bit with the conaffinity of the other, and only a geom with both zero is dropped. Each distinct pair becomes a RaiSim group bit, shared by every MJCF file loaded into the world, with a mask that pairs it accordingly, so geoms of different files also pair up as in MuJoCo; MuJoCo's default pair (1, 1) is RaiSim's default group, and the geoms that touch it also touch RaiSim's ground and heightmaps, which no MJCF geom did. The bundled walker2d.xml, edited to conaffinity="1" to touch its floor, is back to Gymnasium's conaffinity="0": with 1 its legs touch each other, as they would in MuJoCo. See World.
  • Fix: when a frame for a viewer did not fit in RaisimServer's send buffer (a heightmap of 2,900 x 2,900 samples is enough), the server dropped the viewer but kept the object, sensor, visual and chart locks it held while writing the frame. The next viewer then deadlocked the server thread while it held the World lock, so integrateWorldThreadSafe() never returned. The locks are now released, and integrateWorldThreadSafe() also releases the World lock when its callback or the step throws.
  • Fix: ArticulatedSystem::setControlMode() kept the implicit PD terms of the previous mode in the mass matrix. A robot switched from PD to force control still moved as if damped by its gains. Gains set with setPGains() and setDGains() in the force mode and then switched on with setControlMode() acted without their implicit part, so stiff gains diverged: a 0.1 kg pendulum with gains 20 and 5 at a 1 ms step reached 8e6 rad in 50 steps. The implicit terms were also refreshed in World::integrate1() only, so a controller that switched the mode or set gains, damping or springs between integrate1() and integrate2() ran that step with the implicit terms of the previous settings; integrate2() now refreshes them as well.
  • Fix: restoring a World checkpoint kept a granular system's lookup of the grains in each cell as it was in the state left behind. The cells were restored, and the next step kept the lookup while the grains occupied the same cells, so a replay missed most grain contacts (351 of 5068 candidate pairs in a cube of 512 grains) or read past the list of cells. The restore now rebuilds the lookup.
  • Fix: a heightmap's NaN or infinite height samples, which it accepts, now make holes of the cell triangles around them for every body, granular media and rays. A sphere or an ORIGINAL_MESH mesh touching such a triangle got a contact at a NaN position and became NaN within a few hundred steps, a sphere just past the edge of the map, or a cylinder sunk into the terrain, beside such a sample crashed the program, a block of equal infinite samples under a box, capsule or cylinder threw an uncaught exception, a capsule sunk beside such a sample on a large map stopped the program with a non-finite update, a map of such samples only had a NaN bounding box, an infinite sample made the whole map invisible to rays, rays missed the finite triangle sharing a cell with a hole, and a box, capsule, cylinder or convex mesh fell through more or fewer triangles depending on the value and on which corner of a cell held it. Maps without such samples behave exactly as before.
  • Fix: RaiSim read and wrote the numbers of its files in the locale the application set. In a locale with a decimal comma (German, French, Russian and many others; a Qt application sets it from the environment), every single number of a world file, URDF or MJCF read only up to its . (radius="0.5" read 0, mass="1.5" read 1), a list such as xyz="0 0 0.5" was a fatal error, an OBJ mesh lost its vertices, a text heightmap, an MJCF tendon range and an rscene file failed to load, and the OBJ, MTL and CoACD cache files RaiSim writes, the colors RaisimServer sends the viewer and an rscene object's appearance got decimal commas and grouped digits, which the viewer and other readers took for other numbers. Numbers are now read and written with a . decimal point and no digit grouping in every locale. See World Configuration File.
  • Fix (changes names made from template math): a template math expression ({...} in a world or URDF template) was replaced by its result with six decimals, so {pi/1000} became 0.003142 and anything below 5e-7 became 0. The result is now the shortest text that reads back as exactly the same number, so a whole number has no decimal point: a name made from math changes (link_{2*3} is now link_6, was link_6.000000), and other values keep all their digits ({1/3} is 0.3333333333333333, {0.1+0.2} 0.30000000000000004). A result that is not finite ({1/0}) is now a fatal error. See World Configuration File.
  • Fix: the constant pi in a template math expression ({...} in a world or URDF template) never advanced the parser, which then added tokens until it ran out of memory. An unmatched ( or a function name without (, as in {expression}, read past the end of the expression and could crash. Such expressions, and ones that used to load with a part silently dropped (a trailing operator as in {1+}, or a malformed number as in {1.2.3}, read as 1.2), are now fatal errors, so a file containing one no longer loads. Numbers in expressions are now read with . as the decimal point whatever the C locale; under a comma-decimal locale {1.5} was read as 1.
  • Fix: on an elastic deformable (youngsModulus > 0), a distance constraint between coincident particles, which welds a mesh seam, had the compliance 0 / 0, and the whole object turned NaN as soon as the seam pulled apart. It now gets the compliance 1 / (E thickness) of every other length (or is rigid with crossSectionArea). A deformable particle that diverged (a NaN or infinite position or velocity) now makes no contact and leaves the contacts of the rest of its object alone, self contacts included. Its cell in the grids that find self contacts and deformable contacts was the integer conversion of NaN, which wrote far outside the grids and crashed; its contacts with other bodies had NaN positions and full depths; a NaN first particle made the object's bounding box NaN, so the rest of the object fell through the floor; and a particle flung by an infinite force, or by one so large that its squared speed overflows, made the contact margin of the whole object infinite, so every particle touched everything at NaN depths and the object and the bodies lying on it turned NaN. The margin of the other particles now stays finite; a body that a finite but hypersonic particle hits still takes the blow. A rigid body that diverged now stops the simulation with a fatal error whatever the size of the scene and the broadphase; only scenes of 35 or more collision bodies under the default sweep and prune stopped before, while the others gave it contacts at NaN positions, and over a heightmap it read past the height samples. Like every fatal error it calls the fatal callback (RaiSimMsg::setFatalCallback()), which exits the process by default, so one diverged world ends a process that steps several. A body that collides with nothing (collision group or mask 0) is not checked, and one parked at an infinite position no longer stretches a heightmap's bounding box or the multi-box pruning grid to infinity (which stopped the simulation, naming the heightmap). The multi-box pruning grid now spans only the bodies that collide, so scenes using it that hold bodies colliding with nothing find their contacts in a different order and do not reproduce 2.10.2 exactly. AABB::overlaps now reports no overlap for a NaN bound. A self-colliding cloth with particles past +-1e308 no longer hangs.
  • Fix: the acceleration of a spherical joint, which like its velocity is in the child body frame, was added as a world-frame vector to the body accelerations behind getFrameAcceleration(), the IMU and the joint forces of the inverse dynamics (getForceAtJointInWorldFrame(), getTorqueAtJointInWorldFrame()). Away from the identity orientation the accelerations were off, and a free ball joint reported a transmitted torque.
  • Fix: the PD controller and the springs of a spherical joint took the rotation from the joint to its target the long way around whenever the quaternion of that rotation had a negative w, as for a target given as the negative of a quaternion (the same orientation) or a joint that had turned past 180 degrees: a target 10 degrees away was pushed toward 350 degrees the other way, with 35 times the torque.
  • Fix: SingleBodyObject::getVelocity(localIdx, pos_b, vel_w), the velocity of a body point given relative to the body frame origin, added the spin about the origin to the velocity of the center of mass instead of the spin about the center of mass. Bodies with an offset center of mass (meshes, compounds, setCom()) reported wrong point velocities, and tendons attached to them, grains touching them and the viewer's interaction wire used them. For such a body getVelocity(0, {0, 0, 0}, v) is now the velocity of the body frame origin, while getVelocity(0, v) stays the velocity of the center of mass.
  • Fix: ArticulatedSystem setters given a vector of the wrong size (setGeneralizedCoordinate(), setGeneralizedVelocity() and setGeneralizedForce() with an Eigen or VecDyn vector, setState(), setJointDamping(), setActuationLimits(), setRotorInertia()) stored it, and the dynamics then read and wrote past it: 12 torques for an 18-DOF robot overran the forward dynamics' buffers, and setState() with short vectors corrupted the heap. A body index past the last body read or wrote past the body arrays in setExternalForce(), setExternalTorque(), setExternalTorqueInBodyFrame(), setConstraintForce(), setMass() (which corrupted the heap), addSpring(), getDenseJacobian() and getDenseRotationalJacobian(), and in release builds the dense Jacobians were written past a matrix smaller than 3 x DOF. Each is now a fatal error naming the sizes (a dense Jacobian of any size other than 3 x DOF, as debug builds already required); the initializer-list overloads and the PD setters were already fatal.
  • Fix: a cylinder against a triangle mesh (MeshCollisionMode::ORIGINAL_MESH) was tested as the capsule of its axis and radius, with one contact per triangle on the capsule's surface. Standing on a mesh floor it rested on a single contact under its axis, a radius minus a half height too high (a cylinder of radius 0.3 m and height 0.4 m stood 0.1 m above a mesh plate), and toppled at a touch; tilted, its depth was that of the capsule's rounded end; near a mesh it got contacts where the capsule reached past its caps (26 of 512 poses of the torso cylinder of mesh_primitive_proximity, separated from the hand mesh, had some). Each triangle is now tested against the cylinder itself, and a cylinder touches a mesh as a box does: standing on a face around its rim, lying on it along the lowest line of its side, tipped onto its rim at the rim's lowest point, each contact at its own depth, held off a flat mesh surface along its normal. Cylinders now stand, lie, roll and tip off edges on mesh floors of any triangulation as on a box, and a pair allowed fewer than eight contacts (contact::ContactSettings::maxContactsPerPair) keeps its deepest one. mesh_primitive_proximity takes 56% less time for the torso cylinder and 12% more for the limb cylinder, whose 99th percentile goes from 31 to 62 µs; in g1_hand_self_collision the hands move differently, touching on 3327 steps instead of 3312 and sinking about as deep (at most 17 instead of 16 mm), and a step takes 14.4 instead of 12.1 µs (99th percentile 99 instead of 73 µs).
  • Fix: ArticulatedSystem::clearExternalForcesAndTorques() kept the bodies of the cleared forces and torques, and the inverse dynamics attached the ones set afterwards to those bodies, so the joint forces and torques it reports (getForceAtJointInWorldFrame(), getTorqueAtJointInWorldFrame()) stayed wrong until a step ended with an external force set, and repeated clears piled up stale entries; the motion itself was right.
  • Fix: the first update of a SpinningLidar after the sensor was created or loaded swept all the turns made since world time 0: a 10 Hz LiDAR first updated at 30 s, on a robot added to a running world or in a world loaded from a file saved then, cast 300 turns of rays in that update. It now sweeps at most one update interval. A spinning LiDAR of a URDF or MJCF robot also ignored the robot's root body instead of the body it is mounted on, so a LiDAR on any other body, inside the collision body of its link (its housing), hit the housing with every ray. It now ignores the body it is mounted on, as a depth camera does. See Sensors.
  • Fix: the anchor of an MJCF <equality><connect> was taken in the frame RaiSim gives body1, the frame of the body's joint (of its last joint if it has several), instead of body1's own frame, so the pin was off by the joint's pos: a connect at a hinge's pivot, which adds nothing in MuJoCo, held the hinge still. The anchor is now in body1's frame, as in MuJoCo. See World.
  • Fix: with swept CCD (contact::ContactSettings::sweptCcdEnabled), a body approaching the ground or a heightmap more slowly than the speed that closes its gap in one step, as one braked by a force, got an impulse anyway: with no sliding velocity this divided by zero, and the body turned NaN in a step, and a body that can only move along the contact normal, such as a slider, was pulled toward the surface to that speed. Such a body now moves on untouched until the step that would carry it past the surface, which holds it to the speed that reaches the surface.
  • Fix: World::removeObject() left the last step's contacts in place until the next step: the contacts of the objects that touched the removed one pointed at its freed collision bodies (reading the pair's collision body read freed memory), and the contacts naming the removed object or the object moved into its world index by world index named the wrong objects. The last step's contacts (Object::getContacts() and World::getContactProblem()) are now cleared; the next step finds the remaining ones. Removing an object between World::integrate1() and integrate2(), which works on the contacts and constraint rows integrate1() made, now throws std::logic_error, as adding or removing a tendon or a wire there already did.
  • Fix: a revolute joint's axis changed through ArticulatedSystem::getJointAxis_P(), as model randomization does, was ignored when the joint's axis was a coordinate axis (1 0 0, 0 -1 0 and the like) where the model was loaded, as for most robots: a hinge turned from the y axis to the x axis kept turning about y. The new axis is now used from the next kinematics update on (setGeneralizedCoordinate(), updateKinematics(), or the end of the next step), also after a checkpoint captured before that update is restored. Such a checkpoint also restored a closed-loop system's constraint analysis as if no joint placement had changed, so a constraint direction the change made necessary went missing (a tilted parallelogram's closure drifted 1 cm apart); it now restores the pending change. See Modifying the Model in Code.
  • Fix: after World::setWorldTime() moved the time back, as at the start of a new episode, RaiSim-updated spinning LiDARs and depth cameras, and the cameras a RaisimServer viewer renders, stopped updating until the world time passed their last update again, and a LiDAR its owner updated returned empty scans; the IMU already started over. They now update at their next update (the next step for RaiSim-updated sensors), its scan sweeping the time since world time 0 (at most one update interval), and go on at their rates; the new Sensor::isUpdateDue() tells when an update is due. See Sensors.
  • Fix: an articulated system with a joint that moves nothing -- in a URDF, a link without <inertial> on a movable joint, with no mass below it -- turned its whole state into NaN in its first step. Stepping it is now a fatal error naming the joint, as it is for a ball joint or a floating base whose inertia vanishes along some direction (a ball joint carrying a thin rod, a massless base whose only mass hangs from a hinge). A state that has turned NaN for other reasons steps on as before. Such a model still loads, for its kinematics, and a joint whose rotor inertia, damping or PD gains give it an inertia still steps. A URDF mass that is negative or not finite, or an inertia that is not finite, now fails to load. See Creating an Articulated System.
  • Fix: a URDF whose root link is world welded the link below it to the world whatever their joint was: a cart on a prismatic rail from the world lost its rail (a cart-pole had one degree of freedom instead of two), and a floating joint made a fixed base. A floating joint now makes a floating base, starting at the joint's origin, and any other joint that moves keeps world as the fixed, massless base from which the link moves on its joint (an <inertial> on the world link is ignored); a fixed joint is unchanged. See Creating an Articulated System.
  • Fix: an object added between World::integrate1() and integrate2() had none of the state integrate1() makes for every object, and with sleeping enabled integrate2() read its sleep state past the end of the list. Adding an object there now throws std::logic_error, as removing one does, and leaves the world as it was. See World.
  • Fix: World::addGranularBox() checked its options in debug builds only. In a release build a spacing of zero, a negative one, or one too small to move a coordinate of the box (1e-12 m at 1e6 m) never advanced a row and made particles until memory ran out, unless maxParticles stopped it. Every build now rejects these, a radius that is not positive and finite, corners that are not finite or out of order, and options that make no particle.
  • Fix: a granular particle outside a periodic boundary was moved back one width at a time, so a particle far out (one thrown at 1e300 m/s, say) kept the step from ever ending: past 2^53 widths, a width is below the coordinate's resolution and subtracting it changes nothing. It now goes back all the widths at once; one width out, the result is the same as before to the last bit, and a coordinate that is not finite is left as it is. The wrappedParticles statistic still counts the widths. GranularSystem::setPeriodicBoundary() now rejects corners that are not finite.
  • Fix: ArticulatedSystem::massMatrixVecMul() was declared inline in the header but defined only in the library, so a call to it did not link. It is an ordinary member function now, which checks the size of its input and resizes its output.
  • Fix: malformed world files crashed instead of reporting what was wrong. A compound child with more than four object_param values wrote past its parameters, a compound object class that contains itself (directly or through other classes) was expanded from the list it was growing, reading freed memory without end, and a material file without <material> read past an empty list. Each is a fatal error with a message now. A compound child also needs exactly the values its shape takes (box 3, cylinder and capsule 2, sphere 1), finite and positive (a capsule's height may be zero).
  • Fix: contact settings were used unchecked. A broadphase grid of zero cells per axis crashed the first step, and an unknown broadphase type or a NaN cell size silently found no contacts at all. Settings RaiSim cannot use are fatal errors now, from World::setContactSettings(), a world file's <simulation> or a scene alike, and the world keeps its settings. A multi-box pruning grid of more than 2^24 cells (16.8 million; the default is 2^21), which ran out of memory or past the range of its cell indices, now gets larger cells, so a scene that asked for one finds its contacts in a different order. See World.
  • Fix: a STATIC single body kept the external forces and torques set on it, since no step consumed them, and they all acted at once when it turned DYNAMIC: 1000 steps of 1 N moved a 1 kg box at 10 m/s in one step. The list behind getExternalForce() grew by one entry each step. A STATIC body ignores external forces and torques now, and a body that turns STATIC (a compound too) drops the ones still pending. An articulated system took external forces without waking up: asleep, it kept them all, and they acted at once when something woke it (2000 steps of a 0.5 N lift on a sleeping 1 kg robot gave it 0.99 m/s in one step). A force or torque wakes it now, as it does a single body.
  • Fix: ArticulatedSystem::getCompositeCOM()[0] and getCompositeInertia()[0], the whole system's after getMassMatrix(), were wrong: for a fixed-base system the COM kept its first value and the inertia was the root link's own, and for a floating base the inertia was taken about the base joint, not about the composite COM as for every other body.
  • Fix: a URDF spherical joint with a stiffness and no spring_mount got the rest orientation (0, 0, 0, 0), which is no rotation, so its spring did nothing. The rest orientation is the identity now. A spherical joint spring given a zero rest orientation in code (Child::joint.springMount or ArticulatedSystem::addSpring()) is a fatal error, and a non-unit one is normalized. See Creating an Articulated System.
  • Fix: RaisimServer accepted a CR_SET_GC request whose floating-base or ball-joint quaternion was zero (or not of unit length): the robot's whole state turned NaN, which the next contact detection reported as a fatal error. Such a request is rejected now, as CR_SET_POSE's quaternion already was.
  • Fix: when the viewer disconnected while dragging an object, the drag wire stayed, and integrateWorldThreadSafe() kept pulling the object toward where the cursor last was. The wire is released when the client goes.
  • Fix: RaisimServer::processRequests() failing part way through a client's message left the connection open, so a program serving the client from its own loop (acceptConnection() and processRequests(), as synchronous_server_update.cpp does) read the rest of the message as new messages and never replied; acceptConnection() then replaced the client without closing it. A failed processRequests() closes the connection now, and acceptConnection() closes a client still connected. With no client connected, waitForMessageFromClient() returns true at once and processRequests() returns false, so such a loop goes on to accept the next client, as it did when the closed client's connection ended.
  • Fix: a frame larger than the socket's buffers went to the viewer in one blocking send(), so a viewer that stopped reading held the server thread, and killServer(), for as long as it stayed connected. A frame now leaves in pieces as the viewer makes room, within 10 s plus a second per 256 KiB (as a viewer's message must arrive), and killServer() ends the wait within 0.1 s. See Raisim Server.
  • Fix: an open triangle mesh (MeshCollisionMode::ORIGINAL_MESH), such as a plate, dropped on a heightmap sank into it on one side, flipped over and kept spinning (16 rad/s after 3 s). The heightmap is solid below its surface, but only the mesh triangles crossing the surface got contacts, each at a point of the crossing and as deep as the triangle's lowest point. Each part of a mesh triangle over a terrain triangle now touches it at its lowest point, also when it lies wholly below the surface, so open meshes rest on a heightmap as on the ground; closed meshes also get contacts under their bottom faces.
  • Fix: two bodies of an articulated system touching each other kept the first contacts the narrowphase reported, up to ArticulatedSystem::getAllowedNumberOfInternalContactsBetweenTwoBodies() (1 by default), instead of the deepest: a pair touching in two places could keep a 2 cm overlap and leave a 5 cm one unresolved. The deepest are kept now.
  • Fix: with sleeping on (the default), a fixed-base articulated system's base became a moving body for collision: the system's own type, DYNAMIC, was written into every one of its bodies, so a base set into the ground got contacts with it at every step, also after sleeping was turned off. Its base stays STATIC now.
  • Fix: rolling and spinning friction against a kinematic body acted on a body's own turn instead of its turn relative to the kinematic one: a ball at the middle of a kinematic turntable was held still instead of turning with it. They act on the relative turn now, as sliding friction does on the relative velocity.
  • Fix: a contact whose normal impulse dropped to zero during the contact solve kept the rolling and spinning friction impulses that the normal impulse had allowed, a torque at a contact that no longer pressed. They go with the normal impulse now.
  • Fix: swept CCD (contact::ContactSettings::sweptCcdEnabled) took the speed that closes a speculative contact's gap off the bounce, too: a ball 9 cm above the ground at 10 m/s with a restitution of 1 turned at 0.9 m/s in the air, and at 0.5 it was slowed in the air and bounced at 1.2 m/s on the next step. A body that reaches the surface within the step now bounces off it with the surface's restitution, as one touching it would; the bounce starts up to the gap early. The gap's speed was also carried through the error reduction parameter, so with World::setERP(0) a speculative contact stopped the body at its gap; it reaches the surface now. An articulated system integrated with TRAPEZOID still goes in by up to half a step's travel on an impact, as it does without swept CCD; the default SEMI_IMPLICIT stops at the surface (see Collision Detection and Colliders).
  • Fix: a cylinder lying along a mesh edge parallel to its axis (MeshCollisionMode::ORIGINAL_MESH), such as a pipe lying in a slot on its two edges, touched each edge at one point only: it tipped about its two contacts, sank 6 mm between the edges as it settled and was still rocking at 0.2 rad/s after 3 s. Each edge now holds it at both ends of the stretch it lies on, and it rests. A cylinder's side against a mesh edge or vertex also gets its contact midway between the two surfaces; it lay off both by up to half its depth where the edge passed beside the axis. In mesh_primitive_proximity the torso and limb cylinders take 1-2% longer.
  • Fix: GranularSystem::loadGranularState() replaced the particles as it read them, so a checkpoint that failed part way (truncated, or with a bad particle) under a fatal callback that throws left the system half replaced, with tendons attached to its particles pointing past the ones left. It now reads and checks the whole file first and changes nothing unless it all loads. A checkpoint with two particles of one stable id, a particle whose material is not in it, a radius that is not finite and positive, a non-finite particle position or velocity, a density that is not finite and positive (a NaN one loaded and turned the particles NaN) or an unknown normal contact model is an error, and the particle and contact counts are checked against the file's length before anything is allocated. Loading a state also wakes a sleeping system, whose loaded particles stayed frozen.
  • Fix: a granular particle's contact history with a body (the stretch of its tangential spring) was kept under the body's world index, which removing another object gives to the last object: the history of a body that moved to a new index was dropped, and went over to whichever object then had its old one. Histories are kept under the object's id (Object::getId()) now. Histories saved with saveGranularState() by earlier versions match no body after loading and start over.
  • Fix: GJK, the overlap test of convex meshes, triangle meshes and the convex pairs without a routine of their own, missed overlaps of a cylinder shallower than about 0.1 mm. Its search direction shortens as the shapes close in, and a support point depended on that direction's length: a cylinder's support was the center of a cap once the part across its axis was below 1e-4, so a cylinder 0.1 mm or less into a box was missed about once in 10,000 orientations. Capsules, spheres and convex meshes had such cutoffs too, below 1e-6 and 1e-8, which mattered only for overlaps under about 1e-6 m. A support point now depends on the direction alone.
  • Fix: two cylinders with nearly parallel axes touching cap on cap (within 2.5 degrees) got contacts up to 3% of the radius off both surfaces and up to 0.4 mm deeper than they overlapped: the depth was measured from each rim in its cylinder's middle plane, not from the rim of the cap, and the overlap of the caps was taken around the bodies' centers, which a tilt puts beside the caps' centers. Tilted caps touching rim on rim went to the general convex test, whose normal was far from the axes, and equal cylinders one over the other, offset by up to about 1e-12 m, got no contact at all. Each contact is now a point of a cap's rim inside the other cylinder, midway between the two caps: the deepest one (a rim's deepest point or a corner where the rims cross), and others spread over the overlap. The narrowphase of two coaxial cylinders on each other's caps with four contacts takes about 30 ns longer (87 instead of 56 ns for the cap contacts themselves); cylinders side by side take as long as before.
  • Fix: a capsule lying on a plane (the ground, or a level part of a heightmap) got one contact, under whichever end of its axis was lower, so the contact jumped from end to end: a capsule of radius 0.05 m sank 12 mm into the ground in 3 s and kept turning at 0.09 rad/s. Each end that reaches the plane is now a contact, the deeper first. See Collision Detection and Colliders.
  • Fix: a capsule or a cylinder lying on a cylinder's cap or side, or a convex hull resting on a box, a cylinder or a capsule, got a single contact, at a point MPR's portal left off the middle, so the body rolled or rocked: a capsule lying on a cylinder table rolled off it (0.39 m in 3 s) and a hull cube on a box or a cylinder sank 2-4 mm and rocked on one contact a step. These pairs (capsule-cylinder, cylinder-cylinder with axes not parallel, convex mesh-primitive) now get the corners of the overlap of the flat parts that touch, up to four, each at its own depth, with the face's normal over a face; they now rest still. A convex mesh touches with its vertices that reach as deep as the contact, so a faceted or chamfered hull rests on its face, not on the next facets or the chamfers. MPR also lost the contact of a body resting on a large cylinder cap now and then, so it fell for a step: its support for a cylinder took the cap's center for directions within 1e-4 rad of the axis, short of the rim by up to the radius times that angle, which on a 0.5 m cap was more than the resting penetration. The narrowphase of a resting capsule-cylinder pair takes 210 ns instead of 90, and of a cylinder-cylinder pair 230 instead of 110 (two contacts instead of one); pairs that still get one contact take from 4% less to 1.5% longer, and the shape_grid_speed benchmark about 3% longer. The primitives benchmark's pile of 125 bodies settles differently: over eight starts 1 mm apart it runs 9% longer on average (from 21% shorter to 28% longer per start). See Collision Detection and Colliders.
  • Fix: the MJCF reader ignored parts of a model without a word: a body's childclass (its joints and geoms took the main default class, so a menagerie robot lost its joint damping, armature, ranges and axes), a geom's mass (its density was used), a joint's limited="false" (its range still limited it), compiler settotalmass (Gymnasium's HalfCheetah weighed 21.2 kg instead of 14) and the orientation of a worldbody plane (a slope was a level ground). <include> was ignored too, so a menagerie scene.xml loaded only its floor, and of a section repeated by an include, such as the robot's worldbody after the scene's, only the first was read. These now load as in MuJoCo, with one difference: RaiSim does not compute a mesh geom's volume, so a mesh geom ignores a given mass, with a warning. A turned worldbody plane is a fixed articulated system, as a plane on a fixed body is, added after the model's bodies. Since a class now reaches the geoms below a childclass, a size such a geom inherits from a class made for another shape gives the geom what its shape needs, and a mesh geom ignores a size, as in MuJoCo; a mesh geom with a size of its own used to be an error. An MJCF file without a worldbody loads as an empty model; it read past the end of a list. res/mjcfHand/hand.xml includes common/materials.xml, which was missing. See World.
  • Fix: an MJCF body with several joints, turned by axisangle, xyaxes or zaxis, was turned twice: everything past its first joint, the body's geoms and mass included, was turned by the body's orientation once more. Turned by quat or euler, it was right. The loader also wrote its handling of such a body into the model's document, from which the tendons are placed afterwards, so a tendon wrapping a geom on such a body, or below it, missed the geom unless the body sat, unturned, at its parent's origin.
  • Fix: ArticulatedSystem::getVelocity(), getSparseJacobian() and getTimeDerivativeOfSparseJacobian() with Frame::PARENT_FRAME for the root body read the pose of a body before the first (the root has no parent) and returned garbage or crashed. The root's parent frame is now the world frame, as setExternalForce() already took it.
  • Fix: a viewer client that announced a message to RaisimServer and then sent it a byte at a time kept the server thread waiting for as long as it went on sending, since each byte restarted the timeout, and killServer(), which waited for that thread, did not return either. The size of a message and then the rest of it must now each arrive within the timeout plus one second per 256 KiB, so a slow link (at least 256 KiB/s) can still send the viewer's camera images, up to 32 MiB, and killServer() ends a wait for the client's message within 0.1 s. A signal arriving while the server waited for the client, such as a profiler's, no longer drops the viewer, and on Windows the server thread waits for a client message for the given timeout instead of 20 s.
  • Fix: RaisimServer asked the viewer to render the manual RGB and depth cameras of a visual articulated system (addVisualArticulatedSystem()), as it does for the world's robots, but took the images the viewer sent back for a malformed message, as it looked for the camera's robot among the world's objects only, and disconnected the viewer every time a camera was due. The images now reach the visual system's cameras, and needsSensorUpdate() reports a due camera of a visual system too. A reply naming visual tag 0, that of an object never sent to the viewer, is rejected; it was taken for the first such object.
  • Fix: GranularSystem::loadGranularState() replaced a granular system's particles, but tendons attached to them kept their particle indices, which then named other particles or, when the loaded state had fewer, read and wrote past the particles. Tendons now follow their particles, known by the id the system gives each particle as it is added, which a saved state holds, and a tendon whose particle the loaded state does not hold is removed, as removeParticle() already did. Loading during a split integration step throws std::logic_error when a tendon is attached to the system. The legacy wires (addStiffWire() and the like) kept their own particle indices after removeParticle() and loadGranularState() moved their tendons' sites, so a wire measured, and reported through getP1(), getDistance() and the like, another particle or one past the last; they now follow their tendons, and a checkpoint restores them. See Tendons.
  • Fix: removing a granular particle moves the last particle into its index, and the tendon sites on that particle with it. Restoring a World checkpoint captured before the removal brought the particles back but not the sites, so a tendon then pulled on whichever particle had taken the index. Checkpoints now hold the paths of the tendons attached to a granular system, the only ones that change.
  • Fix: a ray passing beside a box, almost parallel to one of its faces, hit the box (World::rayTest(), World::rayTestLidar() and the LiDARs) when its direction crossed that face by a positive amount below 1e-12. The box test took the ray as parallel to the face without checking that it started between the face and the opposite one, so it hit the box extended to infinity across that face. Such rays now miss. The bounding-box test before it hid this for a box aligned with the world axes.

rayrai

  • Fix: the GPU LiDAR (RayraiWindow::measureSpinningLidarSingleDrawGPU()) had the same box test as the CPU ray tests, without the bounding-box test that hid it there: its direction table crosses the faces of a box aligned with the sensor's axes by about 1e-16 in the columns of yaw 90, 180 and 270 degrees, so a box beside one of those rays returned a column of hits from inside the box. It also ignored the robot's root body, as the CPU LiDAR did, instead of the body it is mounted on. It now returns the CPU LiDAR's points in both cases.

TCP viewer

  • The left panel has a collapse button at its top right corner, in addition to collapsing after 3.5 s without the pointer. It sits at the end of the tab row, or of the connect row when there is no session, and works with --keep-overlay-open too; the panel opens again when the pointer comes back over the logo.

Documentation

  • The documentation site moved from Sphinx to VitePress. Pages are Markdown files in docs/sections and keep their URLs. The API reference has a page per class, and animations are MP4 videos instead of GIFs. Building the documentation (RAISIM_DOC=ON, target docs_site) now needs Node.js 18 or newer with npm instead of Sphinx. See Build, Test, and Benchmark.

v2.10.2 (2026-10-22) ​

rayrai

  • A model load that started asynchronously keeps loading that way when later requests are made synchronous, as RayraiWindow::SynchronousMeshLoadingScope and RayraiGlobalAsset::setAsyncMeshLoadingEnabled(false) make them: it still waits for the background decodes of its textures and copies through the staging buffer. Pending loads used to follow the store's current mode, so a scope kept across frames, as the TCP viewer keeps one while a scene loads, made each frame's poll decode the pending loads' textures on the render thread and upload their meshes directly. RayraiWindow::setAsyncMeshLoadingEnabled(false) still finishes every pending load at once, and a synchronous getMeshes() of a file that is still loading finishes that load; the new RayraiGlobalAsset::loadPendingSynchronously() makes every pending load of a store synchronous.

TCP viewer

  • A server's scene loads with shorter frames. Its meshes load within each frame's own polling budget, and the scene loading no longer spends a second 8 ms budget per frame, which it used to spend polling even while every load waited for a worker. The city scene of raisim_scenes in a 1280x720 pane on an RTX 2070 SUPER: median frame 36–40 → 27–29 ms, 95th percentile 279–337 → 161–171 ms, loaded in 768–940 → 712–726 ms.
  • Build: code that compiles the viewer's frame loop (TcpViewerSceneLoading.hpp) calls RayraiWindow::update() after SceneMeshLoading::update() in every frame, as the viewer does, which loads the scene's meshes; SceneMeshLoading::kFrameBudget is removed.

v2.10.1 (2026-10-21) ​

Rebuild applications after updating (RayraiGlobalAsset ABI change).

rayrai

  • Fix: a model that asynchronous loading had copied into the staging buffers crashed the renderer when asynchronous loading was turned off before the model finished, as RayraiWindow::setAsyncMeshLoadingEnabled(false) and the TCP viewer's scene loading do. Its remaining meshes now upload from the staging buffers.
  • Fix: a staged mesh could be destroyed by a decode worker, which has no OpenGL context, when the render thread let go of it while copy helpers were still queued: after finishing the copy itself, or when a load ended without uploading it. Every staged copy now lets go of its mesh on the render thread, and one that is never uploaded (a failed or abandoned load) skips its unstarted copies and gives its staging regions and staged textures back, in the next frame of its own context when another context is current.
  • A RayraiGlobalAsset advances its model loads only in the OpenGL context current at its construction, which it now requires (std::logic_error without one); RayraiWindow's store is made in the window's context. Polled with another context current (pollAsyncLoads(), pollAsyncLoadsUntil(), the new requireUploadContext(), or RayraiWindow::pollAsyncMeshLoads() and each frame's poll), it advances none of its loads, gives their staging space back and throws std::logic_error; once polled in its own context, the loads finish as usual. An instanced visual's automatic mesh LOD upload, tied to the context it began in, behaves the same when polled directly (pollAsyncMeshPreparation(); releaseStagedAsyncMeshUploads() is new), and a draw in another context leaves it pending instead of throwing out of the pass. Loads used to crash, or mix their meshes across contexts, and a poll could tie a load that had not started uploading to the wrong context, after which the whole store stayed stuck.
  • rayrai tells OpenGL contexts apart by the context the OpenGL binding itself reports as current (GLX or EGL, WGL, CGL), not by SDL's record. A context that an embedding host such as Qt or GLFW, or plain EGL, makes current without SDL now counts as its own context. Its texture and mesh caches, upload staging and load ownership work as they do for rayrai's SDL contexts. All such contexts used to count as one, and got no upload staging.
  • Upload staging buffers belong to their OpenGL context wherever it is current, so a context handed to another thread keeps its staging there, also after the thread that made it exited. They were kept per thread: a staged mesh then could not be uploaded, and its staging space was never freed. A store destroyed before its context forgets the context's staging, so contexts made and destroyed leave none behind.

TCP viewer

  • Fix: a URDF or MJCF file that RaiSim cannot load, such as one naming a missing mesh file, ended the viewer when dropped on it or passed with --inspect. The inspector now reports that the load failed, and removes whatever the file had added, such as an MJCF file's floor.
  • The inspector tells MJCF from URDF by the file's root element, as dropping a file already did; a <mujoco> element after a long comment (over 64 KB) used to be taken for a URDF.
  • The inspector (a URDF or MJCF file dropped on a disconnected viewer) loads URDF collision meshes as convex hulls. It never simulates and draws collision bodies from their mesh files, so only loading changes: a 26 MB collision mesh loads in 0.36 s instead of 0.68 s.
  • Build: the inspector's loader moved to TcpViewerInspectorLoad.cpp, which the raisim2Lib rayrai_tcp_viewer example now compiles too.

v2.10.0 (2026-10-19) ​

Rebuild applications after updating (Camera, RaisimObject, RayraiWindow, raisim::World, ArticulatedSystem, RGBCamera, DepthCamera, SpinningLidar and InertialMeasurementUnit ABI changes).

rayrai

  • Fix: animated mesh scales in rayrai_offroad_buggy could discard loaded assets and repeatedly restart asynchronous mesh loading, making the buggy flicker. Scale, local pose and link-attachment edits now retain loaded mesh geometry and refresh bounds/shadows in both bound-World and snapshot rendering. Resizing a capsule or replacing a mesh file keeps unrelated assets alive. The rayrai_animated_mesh_reuse_test regression test and its --benchmark mode cover asset reuse; see Physics snapshots and Performance notes and the C++ API reference.
  • Camera getRawImage() now applies the configured measurement noise in both sensor-setter and CUSTOM_BUFFER modes. GPU LiDAR applies its configured range/angle errors and dropout. Raw readSceneColorBgra() and readLinearDepth() remain clean. Readbacks preserve application packing, draw-buffer and viewport state; software OpenGL uses exact CPU half-float conversion.
  • New: physics snapshots, for rendering on a thread of its own while physics continues. raisin::PhysicsSnapshotCapture::capture(world) copies a World's render state (object and link poses, shapes and appearance, heightmap and deformable vertices, spatial tendons, and the .rscene scene) into an immutable, shared raisin::PhysicsSnapshot, without locks or OpenGL work; RayraiWindow::applyPhysicsSnapshot() shows it on the render thread, and clearPhysicsSnapshot() shows the bound World again. Renders of an applied snapshot, shadows included, never read a World. Camera can be built from copied RGBCameraProperties or DepthCameraProperties, and Camera::readSceneColorBgra() and readLinearDepth() read an image back into application buffers without a RaiSim sensor. A snapshot an application built or edited itself is checked: an object without its description or mesh, malformed vertex blocks, or indices past the vertices raise std::invalid_argument, and the viewer keeps showing the last complete frame. See Physics snapshots.
  • Heightmap and deformable vertices travel in immutable blocks shared between frames (PhysicsMeshSnapshot::blocks), one block per visual row of a heightmap. A capture reads only the heightmap rows whose revision advanced (HeightMap::getHeightRowRevisions()) and refills the visual rows they reach, and a viewer uploads the rows that differ from the ones it shows, so an edit costs what it changes, whatever frames the viewer skipped.
  • Heightmap meshes are built faster: each sample position is computed once instead of once per neighboring vertex (the vertices are bit-identical).
  • Fix: heightmap rows updated in place by a visual patch lost their secondary texture coordinates, which a whole build copies from the primary ones.
  • Fix: when the viewer stopped showing a World's .rscene scene (another World, swapWorld(), or a snapshot without the scene), the scene's local fog volumes, decals, irradiance volumes and reflection probes stayed. A scene's reflection probes capture the objects of the frame that shows the scene.
  • A World bound to the viewer is drawn the same way: each render copies it (updateObjectLists()) and draws the shadow maps and the image from that copy, so one frame always shows one physics state. Images are unchanged (every feature showcase image matches the previous version up to its run-to-run noise); batched spheres now turn with their body, as unbatched ones already did. Moving objects, heightmap edits and deformable terrain got cheaper, unchanged objects slightly dearer (about 7 ns each per render). rayrai_world_sync_benchmark, 320x240 frames on one Ryzen 9 3950X core with an RTX 2070 SUPER: 1,000 boxes with 100 moving 398 → 366 µs per frame, a 257x257 heightmap with a 9x9 visual patch edited every frame 469 → 400 µs, the same edit on a 1025x1025 heightmap 6.23 → 5.11 ms (its copy 2.03 → 0.59 ms), a 257x257 deformable terrain under a sliding plate 458 → 384 µs, 16 integrated ANYmals unchanged at 1.51 ms, and 1,000 idle static boxes 4.7 → 11.8 µs. An upscaled viewer frame (viewerRenderScale below 1) copies the World once.
  • Picking ids are numbered in World order, so the picking view (and 04_picking_ids of the feature showcase) no longer changes from run to run.
  • Fix: setObjectHidden() stayed in effect only until the object's appearance or pose changed, which showed a hidden single body again. Hiding is now kept until setObjectHidden(object, false).
  • Fix: rayrai cleared HeightMap::isUpdated() and the heightmap's visual dirty region, which RaisimServer streams heightmap edits from, so a viewer and a server on one World could each miss edits the other read first. rayrai now leaves these flags alone; it notices heightmap edits by their render revision.
  • A camera following an object (Camera::targetObject) follows the object's position in the rendered frame; the camera never reads the object. A fixed distance locked before the first frame (setCameraFixedDistance(true) right after setTargetObject()) is measured from the object on that frame instead of from the origin. The camera lets the object go when it is removed, when another object takes its address, or when the viewer shows another World. Camera::releaseTarget() clears the target and both locks.
  • renderWithExternalCamera() and renderDepthPlaneDistance() overloads that take a RaiSim sensor, and the GPU lidar, read the live World; they throw std::invalid_argument while an applied snapshot is shown.
  • RaisimObject (internal) holds a copy of its object's snapshot state: it is built from a PhysicsObjectSnapshot, updateAppearance(), updateHeightMapVisual(), isValid() and invalidate() were removed, draw() no longer takes a render serial, and sourceObject() and cullingBounds() were added.
  • raisin::toGlm() moved from RayraiWindow.hpp to helper.hpp, which RayraiWindow.hpp includes; rayrai uses it for every RaiSim-to-GLM conversion. helper.hpp adds HeightmapVisualRowsOfSourceRows() and FillHeightmapVisualRowsRaisim() for filling chosen visual rows of a heightmap.
  • Build: RayraiWindow.cpp shares its post-processing call, world shadow caster drawing, frame cache keys and light hashing between the viewer and external cameras and between shadow passes, instead of keeping copies (20,440 → 19,607 lines).
  • New rayrai_world_sync_benchmark measures the per-frame copy and a small frame of a bound World; every timed step changes the World.

RaiSim

  • Faster primitive contacts with dense triangle meshes: sphere, capsule and cylinder queries skip BVH nodes outside their reach or unable to deepen a full contact set; box queries also cull nodes separated from a padded oriented box. Candidate order, contact limits and merging are preserved. See Collision Detection and Colliders.
  • Fix: box-mesh GJK/EPA fallback could report penetration for a separated triangle. Separating-plane and SAT checks now reject triangles beyond the 0.2 mm tolerance before the fallback.
  • New source benchmarks: mesh_primitive_proximity measures dense G1 hand meshes near five primitives, and g1_hand_self_collision measures a fixed-base, 29-DoF G1 with intermittent hand self-contact. Both compare RaiSim and MuJoCo and report tail timings and contact counts; RaiSim also reports result signatures. See Benchmark.
  • Sensor seed zero now derives a stream from the sensor identity and owning object's stable id. World::setSensorNoiseSeed(uint64_t) decorrelates environments with one call; XML and checkpoints preserve it. Nonzero per-sensor seeds remain explicit, reproducible streams, mixed with the World seed. Worlds with matching seeds and object creation order replay the same noise.
  • Python bindings expose all noise model fields, resets, manual noise application, IMU channel properties and delivery timestamps, and the World sensor seed. Depth properties retain mutable access; RGB, LiDAR and IMU getters return copies for setter-based edits. RGB getImageBuffer() returns Python bytes and setImageBuffer() accepts them, preserving all byte values including alpha; the legacy character-list setter remains accepted. The stale binding to the removed internal contact field MappInv_red is removed; MappInv_i remains available.
  • RaiSimGymTorch assigns distinct World sensor noise seeds per environment, including when user seed hooks override the base. Per-environment user seeds are assigned without the shared increment race; overflowing integer seed ranges are rejected before applying any hooks.
  • World sensor reseeding preserves IMU measurements, filters, bias, acquisition clock and delayed samples. InertialMeasurementUnit::resetNoise() restarts only its random stream. Python RGB, LiDAR and IMU properties are copied for validated setter-based edits; depth retains its mutable API.
  • IMU latency queues grow gradually and retain at most 65,536 samples. Capacity or allocation failures drop new samples with a warning and an observable getDroppedMeasurementCount(), preserving physics stepping and pending deliveries.
  • IMU delivery latency must be between 0 and 60 seconds; larger values are rejected before changing the model. Test RealSense resources explicitly disable camera noise, matching shipped assets.
  • New active sensor models: RGB read, shot and row noise; depth bias, distance-dependent noise, quantization and dropout; LiDAR range bias/noise, angular noise, quantization and dropout. Models run on CPU captures, Rayrai captures and server TCP commits, with independent seeds, reset and checkpoint replay. Removed LiDAR hits shrink the scan. Manual buffer setters remain unchanged. See Sensor noise.
  • Migration: camera noise settings that were previously metadata now affect captures. Shipped RealSense definitions explicitly use type="none" to preserve clean images. Third-party URDF files with nonzero noise must set type="none" to opt out. Camera <noise> is optional; unsupported type strings, including gaussian_quantized, now raise std::invalid_argument.
  • IMUs acquire the completed physics state without enabling inverse dynamics. If an application relied on the loader enabling it for frame-acceleration or inverse-dynamics queries, call setComputeInverseDynamics(true) explicitly. IMU acquisition/filtering does not overwrite those queries' acceleration storage.
  • IMUs add per-channel bias, white-noise density, bias random walk, first-order bandwidth, quantization, per-axis saturation and delivery latency. Saturation now clamps each axis instead of scaling the vector norm: (40, 40, 0) with a 50 m/s² limit remains unchanged. Orientation starts at identity and remains ground truth, not an attitude estimate. getUpdateTimeStamp() is the delivered sample's acquisition time and stays -1 before first delivery; getDeliveryTimeStamp() reports availability. update(World&) now acquires measurements instead of doing nothing. Invalid setProperties() calls throw before changing the model; invalid legacy mutable edits restore the last valid model with a warning.
  • DepthCamera::depthToPointCloud() divides optical depth by the ray's forward component, fixing fisheye point-cloud scale; non-forward rays produce NaNs. Active depth models normalize non-positive/out-of-clip returns to NaN; disabled models preserve raw buffers.
  • After RK4 integration, getGeneralizedAcceleration() reports the completed step's velocity change divided by its time step.
  • New: World::getLifetimeIdentity() identifies a World for its whole lifetime, even when a later World reuses its address, and World::getSharedRscene() shares its immutable .rscene scene.
  • Visual-only heightmap edits (updateVisualHeight(), updateVisualHeightPatch()) and deformable integration advance Object::getRenderRevision().
  • New: HeightMap::getHeightRowRevisions(), one revision per sample row that advances whenever a height in the row changes (by any update method, a checkpoint restore, or a deformable terrain sinking). Unlike isUpdated() and the visual dirty region, the revisions are never cleared, so any number of renderers can follow them. Whole-map updates advance only the rows whose heights differ.
  • updateVisualHeightPatch() that leaves every sample at its value (bit for bit) reports no update: isUpdated(), the visual dirty region and the render revision stay as they were.
  • Fix: ArticulatedSystem::solveIK() that applied a solution moved the links without advancing getRenderRevision(), so renderers that cache by that revision could keep drawing the old pose.
  • Fix: a URDF <sphere>, <cylinder> or <capsule> left the shape parameters it does not set (VisObject::visShapeParam, CollisionDefinition::shapeParam) uninitialized; they are now zero.

RaiSim — physics for robotics and AI.