Skip to content

SkeletonIK


WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

ts
import { Object3D, Scene3D, Engine3D, AtmosphericComponent, CameraUtil, HoverCameraController, View3D, LitMaterial, MeshRenderer, SphereGeometry, Color, Vector3, DirectLight, KelvinUtil, Object3DUtil, AnimatorComponent, TwoBoneIK, PostProcessingComponent, FXAAPost } from "@orillusion/core";
import dat from 'dat.gui';

class Sample_SkeletonIK {
    engine!: Engine3D;
    lightObj3D!: Object3D;
    scene!: Scene3D;
    animator!: AnimatorComponent;
    gui!: dat.GUI;

    async run() {
        const storedType = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('shadowType') : null;
        const shadowType: 'HARD' | 'PCF' | 'SOFT' =
            storedType === 'PCF' || storedType === 'SOFT' ? storedType : 'HARD';

        const engine = this.engine = await Engine3D.init({
            setting: {
                shadow: {
                    type: shadowType,
                    autoUpdate: true,
                    updateFrameRate: 1,
                },
            },
        });

        this.gui = new dat.GUI();

        this.scene = new Scene3D();
        let sky = this.scene.addComponent(AtmosphericComponent);

        let camera = CameraUtil.createCamera3DObject(this.scene);
        camera.perspective(60, engine.aspect, 0.01, 5000.0);

        let ctrl = camera.object3D.addComponent(HoverCameraController);
        ctrl.setCamera(-30, -45, 100);
        ctrl.maxDistance = 1000;

        let view = new View3D();
        view.scene = this.scene;
        view.camera = camera;

        engine.startRenderView(view);

        let postCom = this.scene.addComponent(PostProcessingComponent);
        postCom.addPost(FXAAPost);

        await this.initScene(this.scene);
        sky.relativeTransform = this.lightObj3D.transform;
    }


    async initScene(scene: Scene3D) {
        {
            let man = await this.engine.res.loadGltf('https://cdn.orillusion.com/gltfs/CesiumMan/CesiumMan_compress.gltf');
            man.scaleX = 30;
            man.scaleY = 30;
            man.scaleZ = 30;
            scene.addChild(man);

            let animator = this.animator = man.getComponentsInChild(AnimatorComponent)[0];
            animator.playAnim(animator.clips[0].clipName);

            this.setupFootIK('R');
            this.setupFootIK('L');
        }

        this.scene.addChild(Object3DUtil.GetSingleCube(3000, 1, 3000, 0.5, 0.5, 0.5));

        {
            this.lightObj3D = new Object3D();
            this.lightObj3D.y = 100;
            this.lightObj3D.rotationX = 144;
            this.lightObj3D.rotationY = 0;
            this.lightObj3D.rotationZ = 0;
            let directLight = this.lightObj3D.addComponent(DirectLight);
            directLight.lightColor = KelvinUtil.color_temperature_to_rgb(5355);
            directLight.castShadow = true;
            directLight.intensity = 3;
            directLight.shadowBoundFar = 200;
            directLight.enableCSM = true;
            scene.addChild(this.lightObj3D);
        }

        return true;
    }

    private setupFootIK(side: 'R' | 'L') {
        const hipName = `leg_joint_${side}_1`;
        const kneeName = `leg_joint_${side}_2`;
        const ankleName = `leg_joint_${side}_3`;

        const hip = this.animator.getJointObject(hipName);
        const knee = this.animator.getJointObject(kneeName);
        const ankle = this.animator.getJointObject(ankleName);
        if (!hip || !knee || !ankle) {
            console.warn(`[FootIK] missing bone(s) for side ${side} — IK skipped`);
            return;
        }

        const hipP = this.clonePos(hip);
        const kneeP = this.clonePos(knee);
        const ankleP = this.clonePos(ankle);

        const mid = new Vector3((hipP.x + ankleP.x) * 0.5, (hipP.y + ankleP.y) * 0.5, (hipP.z + ankleP.z) * 0.5);
        const poleDir = new Vector3(kneeP.x - mid.x, kneeP.y - mid.y, kneeP.z - mid.z);
        const legLen = Vector3.distance(hipP, ankleP);
        if (poleDir.length < 1e-4) poleDir.set(0, 0, 1); else poleDir.normalize();
        const poleP = new Vector3(
            kneeP.x + poleDir.x * legLen,
            kneeP.y + poleDir.y * legLen,
            kneeP.z + poleDir.z * legLen,
        );

        const target = this.makeMarker(ankleP, new Color(1.0, 0.4, 0.1, 1), 2.0);
        const pole = this.makeMarker(poleP, new Color(0.1, 0.7, 1.0, 1), 1.4);

        this.scene.addChild(target);

        const solver = new TwoBoneIK({
            name: `foot_${side}`,
            chain: [hipName, kneeName, ankleName],
            target,
            pole,
            weight: 1.0,
        });
        this.animator.addIK(solver);

        let folder = this.gui.addFolder(`Foot IK (${side})`);
        folder.add(solver, 'weight', 0, 1, 0.01).name('weight');
        const range = legLen;
        folder.add(target, 'x', ankleP.x - range, ankleP.x + range, 0.1).name('target x');
        folder.add(target, 'y', ankleP.y - range, ankleP.y + range, 0.1).name('target y');
        folder.add(target, 'z', ankleP.z - range, ankleP.z + range, 0.1).name('target z');
        folder.open();
    }

    private clonePos(obj: Object3D): Vector3 {
        const wp = obj.transform.worldPosition;
        return new Vector3(wp.x, wp.y, wp.z);
    }

    private makeMarker(pos: Vector3, color: Color, radius: number): Object3D {
        const o = new Object3D();
        const mr = o.addComponent(MeshRenderer);
        mr.geometry = new SphereGeometry(radius, 16, 16);
        const mat = new LitMaterial();
        mat.baseColor = color;
        mat.emissiveColor = color;
        mat.emissiveIntensity = 1.5;
        mr.material = mat;
        o.x = pos.x;
        o.y = pos.y;
        o.z = pos.z;
        return o;
    }

}

new Sample_SkeletonIK().run();

Released under the MIT License