1"use strict";(self.webpackChunkrapier=self.webpackChunkrapier||[]).push([[1883],{67511:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>l,default:()=>f,frontMatter:()=>a,metadata:()=>d,toc:()=>u});var n=r(74848),o=r(28453),s=r(11470),i=r(19365);const a={},l=void 0,d={id:"user_guides/c/soft_body_clusters",title:"soft_body_clusters",description:"Joints and rigid colliders both need a frame to be attached to, i.e., a translation and a rotation, which a soft-body",source:"@site/docs/user_guides/c/soft_body_clusters.mdx",sourceDirName:"user_guides/c",slug:"/user_guides/c/soft_body_clusters",permalink:"/docs/user_guides/c/soft_body_clusters",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{}},c={},u=[{value:"The root body",id:"the-root-body",level:3},{value:"Clusters",id:"clusters",level:3}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(t.p,{children:["Joints and rigid colliders both need a frame to be attached to, i.e., a translation and a rotation, which a soft-body\ndoesn't have. This is what ",(0,n.jsx)(t.strong,{children:"soft frames"})," are for: a soft frame is a rigid-body of type\n",(0,n.jsx)(t.code,{children:"R3_SOFT_FRAME"})," (see ",(0,n.jsx)(t.code,{children:"r3RigidBody_IsSoftFrame"}),") which pose is computed at each timestep\nfrom a set of particles, by shape-matching. Since it is an ordinary rigid-body, every API working with rigid-bodies\nworks with it too: ",(0,n.jsx)(t.a,{href:"/docs/user_guides/c/joints",children:"impulse joints"})," of any kind (fixed, revolute, prismatic, generic, etc.) can be\nattached to it, as well as rigid colliders (sensors included), and its position can be read at any time. Therefore a\nsoft-body is linked to another soft-body, to a rigid-body, or to a ",(0,n.jsx)(t.a,{href:"/docs/user_guides/c/joint_constraints#multibodies",children:"multibody"}),"\nexactly the same way two rigid-bodies are."]}),"\n",(0,n.jsx)(t.h3,{id:"the-root-body",children:"The root body"}),"\n",(0,n.jsxs)(t.p,{children:["Every soft-body is created with one soft frame covering all of its particles: its ",(0,n.jsx)(t.strong,{children:"root body"}),". It is the rigid-body\ngiven by ",(0,n.jsx)(t.code,{children:"r3SoftBody_RootBody"}),". A\njoint attached to it acts on the soft-body as a whole, and so does a force or an impulse applied to it. It also stands\nfor the soft-body in the islands, and it is the parent of the colliders the engine built for the body's surface (a\ndeformable collider bound to another ",(0,n.jsx)(t.a,{href:"/docs/user_guides/c/soft_bodies#clusters",children:"cluster"})," has the proxy of that cluster as its parent\ninstead). The soft-body a collider belongs to is given by ",(0,n.jsx)(t.code,{children:"r3Collider_SoftBody"}),", which is how a collider reported by a\n",(0,n.jsx)(t.a,{href:"/docs/user_guides/c/scene_queries",children:"scene query"})," or by a ",(0,n.jsx)(t.a,{href:"/docs/user_guides/c/advanced_collision_detection#collision-and-contact-force-events",children:"collision\nevent"})," is traced back to the body it\ncovers."]}),"\n",(0,n.jsxs)(s.A,{groupId:"dim",defaultValue:"2D",values:[{label:"Example 2D",value:"2D"},{label:"Example 3D",value:"3D"}],children:[(0,n.jsx)(i.A,{value:"2D",children:(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-c",children:"// The rigid-body the engine created for the whole soft-body, read back after its insertion.\nR2RigidBodyHandle root = r2SoftBody_RootBody(jelly_handle);\nassert(r2RigidBody_IsSoftFrame(root));\n\n// A rigid collider attached to it follows the frame of the whole body: here a sensor\n// detecting what comes close to the jelly.\nR2ColliderDesc sensor = r2BallColliderDesc(1.6);\nsensor.isSensor = 1;\nr2InsertCollider(root, &sensor);\n\n// A joint attached to it acts on the soft-body as a whole: this one hangs the jelly under a\n// fixed anchor by a spring.\nR2RigidBodyDesc anchor_desc = r2FixedRigidBodyDesc();\nanchor_desc.position.translation = r2Vector(3.0, 5.0);\nR2RigidBodyHandle anchor = r2InsertRigidBody(world, &anchor_desc);\nR2JointDesc spring = r2SpringJointDesc(2.0, 60.0, 2.0);\nr2InsertImpulseJoint(anchor, root, &spring);\n"})})}),(0,n.jsx)(i.A,{value:"3D",children:(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-c",children:"// The rigid-body the engine created for the whole soft-body, read back after its insertion.\nR3RigidBodyHandle root = r3SoftBody_RootBody(jelly_handle);\nassert(r3RigidBody_IsSoftFrame(root));\n\n// A rigid collider attached to it follows the frame of the whole body: here a sensor\n// detecting what comes close to the jelly.\nR3ColliderDesc sensor = r3BallColliderDesc(1.0);\nsensor.isSensor = 1;\nr3InsertCollider(root, &sensor);\n\n// A joint attached to it acts on the soft-body as a whole: this one hangs the jelly under a\n// fixed anchor by a spring.\nR3RigidBodyDesc anchor_desc = r3FixedRigidBodyDesc();\nanchor_desc.position.translation = r3Vector(3.0, 4.0, 0.0);\nR3RigidBodyHandle anchor = r3InsertRigidBody(world, &anchor_desc);\nR3JointDesc spring = r3SpringJointDesc(2.5, 60.0, 2.0);\nr3InsertImpulseJoint(anchor, root, &spring);\n"})})})]}),"\n",(0,n.jsxs)(t.admonition,{type:"warning",children:[(0,n.jsx)(t.p,{children:"The pose of the root body is recomputed from the particles at each timestep, therefore moving it has no effect."}),(0,n.jsxs)(t.p,{children:["Removing it with ",(0,n.jsx)(t.code,{children:"r3RemoveRigidBody"})," is rejected (with ",(0,n.jsx)(t.code,{children:"R3_INVALID_ARGUMENT"}),"): the soft-body is removed as a whole with ",(0,n.jsx)(t.code,{children:"r3RemoveSoftBody"})," instead (see ",(0,n.jsx)(t.a,{href:"/docs/user_guides/c/soft_bodies#removal",children:"removal"}),")."]})]}
1),"\n",(0,n.jsx)(t.h3,{id:"clusters",children:"Clusters"}),"\n",(0,n.jsxs)(t.p,{children:["A single frame for the whole body is often not expressive enough: several joints attached to the root body all act on\nthe body as a whole, and their effect isn't concentrated where they are attached. This is why a soft-body can also be\ngiven ",(0,n.jsx)(t.strong,{children:"clusters"})," (",(0,n.jsx)(t.code,{children:"r3SoftBody_AddCluster"}),"), i.e.,\nsoft frames over any subset of its particles, each with its own pose computed by shape-matching over that subset only.\nJoints attached to different clusters then act on different parts of the body, each with its own orientation:"]}),"\n",(0,n.jsx)("center",{children:(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"One soft frame per cluster",src:r(18843).A+"",width:"940",height:"393"})})}),"\n",(0,n.jsx)(t.p,{children:"Similarly, rigid colliders attached to the proxies of different clusters move and rotate independently, which is what\nallows the definition of rigid parts on a deformable body: the handle of a deformable hammer, the bones of a soft\ncharacter, or the plate a jelly is carried on."}),"\n",(0,n.jsx)("center",{children:(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Rigid colliders attached to different soft frames",src:r(13845).A+"",width:"940",height:"340"})})}),"\n",(0,n.jsxs)(t.p,{children:["A cluster is identified by its index in the soft-body, returned by ",(0,n.jsx)(t.code,{children:"r3SoftBody_AddCluster"}),", and its proxy is given by\n",(0,n.jsx)(t.code,{children:"r3SoftBody_ClusterProxy"}),". The indices of the live clusters of a body are given by ",(0,n.jsx)(t.code,{children:"r3SoftBody_Clusters"}),", and the\nparticles of one of them by ",(0,n.jsx)(t.code,{children:"r3SoftBody_ClusterParticles"}),":"]}),"\n",(0,n.jsxs)(s.A,{groupId:"dim",defaultValue:"2D",values:[{label:"Example 2D",value:"2D"},{label:"Example 3D",value:"3D"}],children:[(0,n.jsx)(i.A,{value:"2D",children:(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-c",children:"// A cluster over the top particles of the jelly: a rigid proxy that joints and\n// colliders can attach to.\nsize_t num_jelly_particles = r2SoftBody_NumParticles(jelly_handle);\nuint32_t *top = malloc(num_jelly_particles * sizeof(uint32_t));\nsize_t num_top = 0;\nfor (uint32_t i = 0; i < num_jelly_particles; i++) {\n if (r2SoftBody_ParticlePosition(jelly_handle, i).y > 2.0) {\n top[num_top++] = i;\n }\n}\nuint32_t cluster = r2SoftBody_AddCluster(jelly_handle, top, num_top);\nfree(top);\nR2RigidBodyHandle proxy = r2SoftBody_ClusterProxy(jelly_handle, cluster);\n// A rigid plate welded onto the cluster.\nR2RigidBodyDesc plate_desc = r2DynamicRigidBodyDesc();\nplate_desc.position.translation = r2Vector(3.0, 2.4);\nR2RigidBodyHandle plate = r2InsertRigidBody(world, &plate_desc);\nR2ColliderDesc plate_collider = r2CuboidColliderDesc(r2Vector(1.2, 0.05));\nplate_collider.density = 0.4;\nr2InsertCollider(plate, &plate_collider);\nR2JointDesc weld = r2FixedJointDesc();\nweld.localFrame1.translation = r2Vector(0.0, -0.1);\nr2InsertImpulseJoint(plate, proxy, &weld);\n// A cluster can be pinned, driven or tuned as a whole.\nr2SoftBody_SetClusterStiffnessScale(jelly_handle, cluster, 2.0);\nr2SoftBody_SetClusterShapeMatchingEnabled(jelly_handle, cluster, 1);\n"})})}),(0,n.jsx)(i.A,{value:"3D",children:(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-c",children:"// A cluster over the top particles of the jelly: a rigid proxy that joints and\n// colliders can attach to.\nsize_t num_jelly_particles = r3SoftBody_NumParticles(jelly_handle);\nuint32_t *top = malloc(num_jelly_particles * sizeof(uint32_t));\nsize_t num_top = 0;\nfor (uint32_t i = 0; i < num_jelly_particles; i++) {\n if (r3SoftBody_ParticlePosition(jelly_handle, i).y > 1.3) {\n top[num_top++] = i;\n }\n}\nuint32_t cluster = r3SoftBody_AddCluster(jelly_handle, top, num_top);\nfree(top);\nR3RigidBodyHandle proxy = r3SoftBody_ClusterProxy(jelly_handle, cluster);\n// A rigid plate welded onto the cluster.\nR3RigidBodyDesc plate_desc = r3DynamicRigidBodyDesc();\nplate_desc.position.translation = r3Vector(3.0, 1.9, 0.0);\nR3RigidBodyHandle plate = r3InsertRigidBody(world, &plate_desc);\nR3ColliderDesc plate_collider = r3CuboidColliderDesc(r3Vector(0.7, 0.05, 0.7));\nplate_collider.density = 0.4;\nr3InsertCollider(plate, &plate_collider);\nR3JointDesc weld = r3FixedJointDesc();\nweld.localFrame1.translation = r3Vector(0.0, -0.1, 0.0);\nr3InsertImpulseJoint(plate, proxy, &weld);\n// A cluster can be pinned, driven or tuned as a whole.\nr3SoftBody_SetClusterStiffnessScale(jelly_handle, cluster, 2.0);\nr3SoftBody_SetClusterShapeMatchingEnabled(jelly_handle, cluster,
11);\n"})})})]}),"\n",(0,n.jsx)(t.p,{children:"A cluster also defines a few settings for the elements it covers, which gives regional materials without needing\nseparate bodies:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["The ",(0,n.jsx)(t.strong,{children:"stiffness scale"}),"\n(",(0,n.jsx)(t.code,{children:"r3SoftBody_SetClusterStiffnessScale"}),")\nmultiplies the Young modulus of every cell entirely contained in the cluster (the cells straddling its boundary are\nleft unchanged)."]}),"\n",(0,n.jsxs)(t.li,{children:["The ",(0,n.jsx)(t.strong,{children:"edge softness"}),"\n(",(0,n.jsx)(t.code,{children:"r3SoftBody_SetClusterEdgeSoftness"}),")\noverrides the softness of every edge entirely contained in the cluster, e.g., a stiffer collar on a shirt."]}),"\n",(0,n.jsxs)(t.li,{children:["The ",(0,n.jsx)(t.strong,{children:"tear resistance"}),"\n(",(0,n.jsx)(t.code,{children:"r3SoftBody_SetClusterTearResistance"}),")\nmultiplies the tear thresholds of every element entirely contained in the cluster, e.g., a tough region, or a\nperforation line."]}),"\n",(0,n.jsxs)(t.li,{children:[(0,n.jsx)(t.strong,{children:"Shape-matching"})," (",(0,n.jsx)(t.code,{children:"r3SoftBody_SetClusterShapeMatchingEnabled"}),") pulls the particles of the cluster toward the frame of its\nproxy (or toward the target pose given by ",(0,n.jsx)(t.code,{children:"r3SoftBody_SetClusterShapeMatchingTarget"}),"), so that part of the body tends to keep the shape it was\ncreated with."]}),"\n"]}),"\n",(0,n.jsx)(t.admonition,{type:"warning",children:(0,n.jsx)(t.p,{children:"The rotation of a cluster is deduced from its particles, which isn't possible for a cluster made of a single particle\n(or, in 3D, of collinear particles). Such a cluster has no angular response, therefore the angular parts of the joints\nattached to its proxy are disabled."})})]})}function f(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}},19365:(e,t,r)=>{r.d(t,{A:()=>i});r(96540);var n=r(18215);const o={tabItem:"tabItem_Ymn6"};var s=r(74848);function i(e){let{children:t,hidden:r,className:i}=e;return(0,s.jsx)("div",{role:"tabpanel",className:(0,n.A)(o.tabItem,i),hidden:r,children:t})}},11470:(e,t,r)=>{r.d(t,{A:()=>v});var n=r(96540),o=r(18215),s=r(23104),i=r(56347),a=r(205),l=r(57485),d=r(31682),c=r(70679);function u(e){return n.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,n.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad <Tabs> child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:t,children:r}=e;return(0,n.useMemo)((()=>{const e=t??function(e){return u(e).map((e=>{let{props:{value:t,label:r,attributes:n,default:o}}=e;return{value:t,label:r,attributes:n,default:o}}))}(r);return function(e){const t=(0,d.XI)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in <Tabs>. Every value needs to be unique.`)}(e),e}),[t,r])}function f(e){let{value:t,tabValues:r}=e;return r.some((e=>e.value===t))}function p(e){let{queryString:t=!1,groupId:r}=e;const o=(0,i.W6)(),s=function(e){let{queryString:t=!1,groupId:r}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!r)throw new Error('Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:t,groupId:r});return[(0,l.aZ)(s),(0,n.useCallback)((e=>{if(!s)return;const t=new URLSearchParams(o.location.search);t.set(s,e),o.replace({...o.location,search:t.toString()})}),[s,o])]}function y(e){const{defaultValue:t,queryString:r=!1,groupId:o}
1=e,s=h(e),[i,l]=(0,n.useState)((()=>function(e){let{defaultValue:t,tabValues:r}=e;if(0===r.length)throw new Error("Docusaurus error: the <Tabs> component requires at least one <TabItem> children component");if(t){if(!f({value:t,tabValues:r}))throw new Error(`Docusaurus error: The <Tabs> has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${r.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const n=r.find((e=>e.default))??r[0];if(!n)throw new Error("Unexpected error: 0 tabValues");return n.value}({defaultValue:t,tabValues:s}))),[d,u]=p({queryString:r,groupId:o}),[y,g]=function(e){let{groupId:t}=e;const r=function(e){return e?`docusaurus.tab.${e}`:null}(t),[o,s]=(0,c.Dv)(r);return[o,(0,n.useCallback)((e=>{r&&s.set(e)}),[r,s])]}({groupId:o}),m=(()=>{const e=d??y;return f({value:e,tabValues:s})?e:null})();(0,a.A)((()=>{m&&l(m)}),[m]);return{selectedValue:i,selectValue:(0,n.useCallback)((e=>{if(!f({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),g(e)}),[u,g,s]),tabValues:s}}var g=r(92303);const m={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var b=r(74848);function _(e){let{className:t,block:r,selectedValue:n,selectValue:i,tabValues:a}=e;const l=[],{blockElementScrollPositionUntilNextRender:d}=(0,s.a_)(),c=e=>{const t=e.currentTarget,r=l.indexOf(t),o=a[r].value;o!==n&&(d(t),i(o))},u=e=>{let t=null;switch(e.key){case"Enter":c(e);break;case"ArrowRight":{const r=l.indexOf(e.currentTarget)+1;t=l[r]??l[0];break}case"ArrowLeft":{const r=l.indexOf(e.currentTarget)-1;t=l[r]??l[l.length-1];break}}t?.focus()};return(0,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.A)("tabs",{"tabs--block":r},t),children:a.map((e=>{let{value:t,label:r,attributes:s}=e;return(0,b.jsx)("li",{role:"tab",tabIndex:n===t?0:-1,"aria-selected":n===t,ref:e=>l.push(e),onKeyDown:u,onClick:c,...s,className:(0,o.A)("tabs__item",m.tabItem,s?.className,{"tabs__item--active":n===t}),children:r??t},t)}))})}function j(e){let{lazy:t,children:r,selectedValue:s}=e;const i=(Array.isArray(r)?r:[r]).filter(Boolean);if(t){const e=i.find((e=>e.props.value===s));return e?(0,n.cloneElement)(e,{className:(0,o.A)("margin-top--md",e.props.className)}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:i.map(((e,t)=>(0,n.cloneElement)(e,{key:t,hidden:e.props.value!==s})))})}function x(e){const t=y(e);return(0,b.jsxs)("div",{className:(0,o.A)("tabs-container",m.tabList),
1children:[(0,b.jsx)(_,{...t,...e}),(0,b.jsx)(j,{...t,...e})]})}function v(e){const t=(0,g.A)();return(0,b.jsx)(x,{...e,children:u(e.children)},String(t))}},18843:(e,t,r)=>{r.d(t,{A:()=>n});const n=r.p+"assets/images/multi_clusters-fa629042885822a398d43864447f3cca.svg"},13845:(e,t,r)=>{r.d(t,{A:()=>n});const n=r.p+"assets/images/multi_colliders-f4c175e899ca646f98a569c274cb3d4e.svg"},28453:(e,t,r)=>{r.d(t,{R:()=>i,x:()=>a});var n=r(96540);const o={},s=n.createContext(o);function i(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.