qttn.dev

逆運動学

タエチャスク ナッチャノン 05241011

はじめに

Parametric Curveで WebGL の基本的な描画方法を学びましたが、今回は 3D での実装を行うため、ボイラープレートコードの用意を不要にするため、Three.js を使用します。

左側にオレンジ色の箱が表示されていると思います。これは以下のコードだけで表示できます。

import { Canvas } from "@react-three/fiber";
import { OrbitControls, Box } from "@react-three/drei";

export function InverseKinematics() {
  return (
    <div style={{ width: "100%", height: "100%" }}>
      <Canvas camera={{ position: [3, 3, 3] }}>
        <ambientLight intensity={0.5} />
        <directionalLight position={[10, 10, 5]} intensity={1} />

        <Box args={[1, 1, 1]} position={[0, 0.5, 0]}>
          <meshStandardMaterial color="orange" />
        </Box>

        <Grid args={[100, 100]} />

        <OrbitControls enablePan={true} enableZoom={true} enableRotate={true} />
      </Canvas>
    </div>
  );
}

非常に、簡単だったと思います。

目標

逆運動学、または Inverse InverseKinematics (IK) とは、特定の位置に複数の関節を持つロボットアームなどを配置するための技術であるため、今回はロボットアームを作成し、ボールを掴むようにします。

まずは、ロボットアームの基本的な構造を作成しましょう。

ロボットアームの作成

5つの関節を持つロボットアームを作成します。各関節は異なる色で表示され、長さはすべて 0.5 にする。 各関節は、自分の子を​ tip に配置し、次々の関節を再帰的に形成します。

const colors = ["orange", "blue", "green", "red", "purple"];
const lengths = [0.5, 0.5, 0.5, 0.5, 0.5];

function RobotArm({ target }: { target?: Vector3 }) {
  const { scrollPassed } = useScrollDetectorState();

  const [rotations, setRotations] = useState(
    colors.map(() => new Quaternion()),
  );

  const jointRefs = useRef([
    ...colors.map(() => createRef<Mesh>()),
    createRef<Mesh>(),
  ]);

  return (
    <group>
      <RobotJoint
        colors={colors}
        lengths={lengths}
        rotations={rotations}
        refs={jointRefs.current}
      />
    </group>
  );
}

function RobotJoint({
  colors,
  lengths,
  rotations,
  refs,
}: {
  colors: string[];
  lengths: number[];
  rotations: Quaternion[];
  refs: React.RefObject<Mesh | null>[];
}) {
  const length = lengths[0];
  const color = colors[0];
  const rotation = rotations[0];
  const ref = refs[0];

  return (
    <group ref={ref} quaternion={rotation}>
      <Box args={[0.2, length, 0.2]} position={[0, length / 2, 0]}>
        <meshStandardMaterial color={color} />
      </Box>

      <group position={[0, length, 0]}>
        {lengths.length > 1 && (
          <RobotJoint
            colors={colors.slice(1)}
            lengths={lengths.slice(1)}
            rotations={rotations.slice(1)}
            refs={refs.slice(1)}
          />
        )}
        <mesh ref={refs.slice(1).length === 1 ? refs.slice(1)[0] : undefined}>
          <sphereGeometry args={[0.1, 32, 32]} />
          <meshStandardMaterial color={color} />
        </mesh>
      </group>
    </group>
  );
}

最後の ref は、最後ロボット関節のエンドエフェクター (tip) を表します。

ターゲット

次に、ターゲットを作成します。ターゲットは、ロボットアームが掴むべき位置を示すためのものです。

function Target() {
  return (
    <mesh position={[0, 0.5, 2]}>
      <sphereGeometry args={[0.2, 32, 32]} />
      <meshStandardMaterial color="yellow" />
    </mesh>
  );
}

ただし、DragControls を普通に使うと、OrbitControls と競合してしまうため、setDragging を使って、ドラッグ中は OrbitControls を無効にするようにします。

export function InverseKinematics() {
  const [dragging, setDragging] = useState(false);

  return (
    // ...
        <Target setDragging={setDragging} />
        <OrbitControls
          enabled={!dragging}
        />
  );
}

export function Target({
  setDragging,
}: {
  setDragging: (dragging: boolean) => void;
}) {
  return (
    <DragControls
      onDragStart={() => setDragging(true)}
      onDragEnd={() => setDragging(false)}
    >
      // ...
    </DragControls>
  );
}

逆運動学の実装

逆運動学のコアは、ターゲットに対して回転させ、複数回反復して、各関節の回転を更新するだけで非常に簡単ですが、

const updateRotations = useCallback(() => {
  if (!target) return;

  const endIndex = colors.length - 1;
  const end = jointRefs.current[jointRefs.current.length - 1].current;
  if (!end) return;

  const newRotations = rotations.map((r) => r.clone());

  for (let iter = 0; iter < MAX_ITERATIONS; iter++) {
    for (let i = endIndex; i >= 0; i--) {
      const endEffector = end.getWorldPosition(new Vector3());
      const joint = jointRefs.current[i].current;
      if (!joint) continue;
      const ev = joint.worldToLocal(endEffector.clone()).normalize();
      const tv = joint.worldToLocal(target.clone()).normalize();
      const q = new Quaternion().setFromUnitVectors(ev, tv);
      newRotations[i].multiply(q);
      joint.quaternion.copy(newRotations[i]);
    }
  }

  setRotations(newRotations);
}, [target, rotations]);

useEffect(() => {
  if (target) {
    updateRotations();
  }
}, [target]);

ただし、これではロボットアームの関節が無条件で回転してしまうため、関節の回転を制限する必要があります。 まずは、ヒンジ関節を実装します。

const invq = joint.quaternion.clone().invert();
const axis = i === 0 ? new Vector3(0, 1, 0) : new Vector3(0, 0, 1);
const parent = axis.clone().applyQuaternion(invq);
q.setFromUnitVectors(axis, parent);
newRotations[i].multiply(q);
joint.quaternion.copy(newRotations[i]);

ベース関節だけは Y 軸を中心に回転し、それ以外の関節は Z 軸を中心に回転するようにします。 次は、関節を -PI/2 から PI/2 の範囲に制限します。

if (i !== 0) {
  const euler = joint.rotation;
  const clamped = new Vector3()
    .setFromEuler(euler)
    .clamp(
      new Vector3(-Math.PI / 2, -Math.PI / 2, -Math.PI / 2),
      new Vector3(Math.PI / 2, Math.PI / 2, Math.PI / 2),
    );
  newRotations[i].setFromEuler(new Euler(clamped.x, clamped.y, clamped.z));
  joint.quaternion.copy(newRotations[i]);
}

これで、関節の回転が制限され、ロボットアームがターゲットに向かって正しく動くようになります。

Reference: https://rodolphe-vaillant.fr/entry/114/cyclic-coordonate-descent-inverse-kynematic-ccd-ik

以上は Shader Toy での実装だそうですが、pseudo code として参考にしました。