I'm working on a custom "cloth" implementation. I have pretty much everything working except for a couple things:
When I update the number of segments within the plane/cloth using the UI, the y direction works perfectly, but when changing the x segment count, the first row which is initialized using type={isInFirstRow ? 'kinematicPosition' : 'dynamic'} would stay static if I put the property directly on the component. I ended up removing that prop from the and putting that logic into an effect to make it work properly using: currVertex.raw().setBodyType(isInFirstRow ? 2 : 0);
Now that I have that updating BETTER (still not perfect), it only updates correctly as I lower the amount. If I increase the number of x segments, the top row does not increase and the physics becomes erratic. You can also see that the spheres are orange (representing type of 'kinematicPosition') in the top row, but also start to wrap down to the lower rows if I start increases the segments.
I had a feeling that the reason that both of the previous issues were happening because I am using the index of the array item for the key on the root React component when I iterate over the array of rigidbodies. I changed this from:
clothRbVerticesRefs.current.map((item, i) => {
return (
<Fragment key={i}>
<RigidBody
to this:
clothRbVerticesRefs.current.map((item, i) => {
return (
<Fragment key={item?.handle ? `${item.handle}` : i}>
<RigidBody
but as soon as I do that, the rigidbodies actually disappear from my screen when using <Debug />
. Here is my full file:
import type { MeshProps } from '@react-three/fiber';
import { useFrame } from '@react-three/fiber';
import type { RigidBodyApi } from '@react-three/rapier';
import { Debug, RigidBody, useSphericalJoint } from '@react-three/rapier';
import { Fragment, useCallback, useEffect, useMemo, useRef } from 'react';
import type { Mesh } from 'three';
import { CollisionGroups } from '@app/constants';
import type { Vector3Object } from '@app/types';
import { getCollisionGroups, useElementArrayRef } from '@app/utils';
import { SphericalJoint } from './SphericalJoint';
interface ClothProps extends Omit<MeshProps, 'geometry'> {
width: number;
height: number;
xSegments: number;
ySegments: number;
mass: number;
windForce?: Vector3Object;
}
export function Cloth(props: ClothProps): JSX.Element {
const { children, width, height, xSegments, ySegments, ...rest } = props;
const { clothRef, clothRbVertices } = useCloth(props);
return (
<group>
<Debug />
<mesh ref={clothRef} receiveShadow castShadow {...rest}>
<planeGeometry args={[width, height, xSegments, ySegments]} />
{clothRbVertices}
{children}
</mesh>
</group>
);
}
const clothColliderGroups = getCollisionGroups(CollisionGroups.Cloth, ~CollisionGroups.Cloth);
function useCloth({ xSegments, ySegments, mass, width, height, windForce }: ClothProps) {
const clothRef = useRef<Mesh>(null);
const xVertexCount = xSegments + 1;
const yVertexCount = ySegments + 1;
const faceWidth = width / xSegments;
const faceHeight = height / ySegments;
const totalVertices = xVertexCount * yVertexCount;
const {
initialized: clothRbVerticesInitialized,
refs: clothRbVerticesRefs,
refFunction,
} = useElementArrayRef<RigidBodyApi>(totalVertices, [totalVertices]);
useEffect(() => {
if (!clothRbVerticesInitialized) return;
// initialize physics rb positions to match the plane mesh's vertices positions
clothRbVerticesRefs.current.forEach((currVertex, i) => {
if (currVertex != null && clothRef.current != null) {
const { position: pos } = clothRef.current.geometry.attributes;
currVertex.setTranslation({ x: pos.getX(i), y: pos.getY(i), z: pos.getZ(i) });
const isInFirstRow = i < xVertexCount;
// 2 is 'kinematicPosition' and 0 is 'dynamic'. Doing this here instead of on the component as props because it isn't updating properly there
currVertex.raw().setBodyType(isInFirstRow ? 2 : 0);
}
});
}, [clothRbVerticesInitialized, clothRbVerticesRefs, xVertexCount]);
useFrame(() => {
if (!clothRbVerticesInitialized) return;
clothRbVerticesRefs.current.forEach((currentRb, i) => {
if (currentRb != null && clothRef.current != null) {
if (windForce != null) {
currentRb.resetForces();
const { x, y, z } = windForce;
if (x !== 0 || y !== 0 || z != 0) {
const randValue = Math.random();
currentRb.addForce({
x: x * randValue,
y: y * randValue,
z: z * randValue,
});
}
}
const { x, y, z } = currentRb.translation();
clothRef.current.geometry.attributes.position.setXYZ(i, x, y, z);
clothRef.current.geometry.attributes.position.needsUpdate = true;
}
});
});
const getJointToNextVertex = useCallback(
(i: number) => {
if (!clothRbVerticesInitialized) return null;
const currVertex = clothRbVerticesRefs.current[i];
const nextVertex = clothRbVerticesRefs.current[i + 1];
if (currVertex != null && nextVertex != null && (i + 1) % xVertexCount !== 0) {
const currVertexAnchorPos = {
// the attaching joint should be the right side of the face
x: faceWidth / 2,
y: 0,
z: 0,
};
const nextVertexAnchorPos = {
// the attaching joint should be the left side of the face
x: -faceWidth / 2,
y: 0,
z: 0,
};
return (
<SphericalJoint
body1={currVertex}
body2={nextVertex}
body1Anchor={currVertexAnchorPos}
body2Anchor={nextVertexAnchorPos}
/>
);
}
return null;
},
[clothRbVerticesInitialized, clothRbVerticesRefs, faceWidth, xVertexCount]
);
const getJointToBelowVertex = useCallback(
(i: number) => {
if (!clothRbVerticesInitialized) return;
const currVertex = clothRbVerticesRefs.current[i];
const belowVertex = clothRbVerticesRefs.current[i + xVertexCount];
if (currVertex != null && belowVertex != null) {
const currVertexAnchorPos = {
x: 0,
// the attaching joint should be the bottom side of the face
y: -faceHeight / 2,
z: 0,
};
const belowVertexAnchorPos = {
x: 0,
// the attaching joint should be the top side of the face
y: faceHeight / 2,
z: 0,
};
return (
<SphericalJoint
body1={currVertex}
body2={belowVertex}
body1Anchor={currVertexAnchorPos}
body2Anchor={belowVertexAnchorPos}
/>
);
}
return null;
},
[clothRbVerticesInitialized, clothRbVerticesRefs, faceHeight, xVertexCount]
);
const clothRbVertices = useMemo(
() =>
clothRbVerticesRefs.current.map((item, i) => {
return (
<Fragment key={item?.handle ? `${item.handle}` : i}>
<RigidBody
ref={(ref) => refFunction(ref, i)}
colliders="ball"
collisionGroups={clothColliderGroups}
includeInvisible
mass={mass / totalVertices}
// TODO: test out higher linear dampening, high mass, with higher forces
linearDamping={0.8}
>
<mesh visible={false}>
<sphereGeometry args={[0.1]} />
<meshBasicMaterial wireframe />
</mesh>
</RigidBody>
{getJointToNextVertex(i)}
{getJointToBelowVertex(i)}
</Fragment>
);
}),
[
clothRbVerticesRefs,
getJointToBelowVertex,
getJointToNextVertex,
mass,
refFunction,
totalVertices,
]
);
return { clothRef, clothRbVertices };
}
export function useElementArrayRef<RefType = unknown>(
length: number,
deps: DependencyList = emptyArray
): {
refs: React.MutableRefObject<(RefType | null)[]>;
refFunction: (ref: RefType | null, refIndex: number) => void;
initialized: boolean;
} {
const refs = useRef<(RefType | null)[]>(Array(length).fill(null));
const initializedRefCount = useRef(0);
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (refs.current.length !== length) {
refs.current = Array(length).fill(null);
setInitialized(false);
initializedRefCount.current = 0;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [length, ...deps]);
const refFunction = useCallback(
(ref: RefType | null, refIndex: number) => {
if (!initialized && initializedRefCount.current < refs.current.length) {
refs.current[refIndex] = ref;
if (ref != null) {
initializedRefCount.current++;
}
if (initializedRefCount.current === refs.current.length) {
setInitialized(true);
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[initialized, ...deps]
);
return { refs, refFunction, initialized };
}
export const SphericalJoint = memo(function SphericalJoint({
body1,
body2,
body1Anchor,
body2Anchor,
}: {
body1: RigidBodyApi;
body2: RigidBodyApi;
body1Anchor: Vector3Object;
body2Anchor: Vector3Object;
}) {
const { x: x1, y: y1, z: z1 } = body1Anchor;
const { x: x2, y: y2, z: z2 } = body2Anchor;
const aRef = useRef(body1);
const bRef = useRef(body2);
useSphericalJoint(aRef, bRef, [
[x1, y1, z1],
[x2, y2, z2],
]);
return null;
});
I don't see a way to upload a video to display the behavior here, so I will link to this same question on the Github discussions for the react-three-rapier library where there is a video posted at the end showing the behavior I'm seeing:
https://github.com/pmndrs/react-three-rapier/discussions/188
Thank you in advance for any help or tips you can provide!