Three.js鼠标拖动设置骨骼姿态

在这里插入图片描述
在这里插入图片描述

Three.js鼠标拖动设置骨骼姿态方案1

实现

  • 根据SkinnedMesh生成Mesh 作为射线检测的目标(射线检测SkinnedMesh存在不足 无法应用骨骼形变的顶点 )
  • 点击模型 获取点击位置对应的骨骼
  • 拖拽鼠标设置骨骼旋转角度(使用TransformControl选中点击的骨骼 设置轴为XYZE 并隐藏控件 主动触发控件对应的方法 模拟拖拽控件的过程 )
  • 每次变更后更新生成的Mesh

细节

  • 更改TransformControls源码 屏蔽原本行为
  • 生成的Mesh需要使用新的类 这个类继承THREE.Mesh 覆盖其raycast方法 优化射线检测需要更新boundingBox和boundingSphere所需的开销 跳过boundingShere检测过程

源码

  • gitee
    运行项目后访问地址为http://localhost:3000/transform

核心代码

import * as THREE from "three";
import { ThreeHelper } from "@/src/ThreeHelper";
import { MethodBaseSceneSet, LoadGLTF } from "@/src/ThreeHelper/decorators";
import { MainScreen } from "./Canvas";
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
import { CCDIKSolver, CCDIKHelper } from "@/src/ThreeHelper/addons/CCDIKSolver";
import EventMesh from "@/src/ThreeHelper/decorators/EventMesh";
import type { BackIntersection } from "@/src/ThreeHelper/decorators/EventMesh";
import { Injectable } from "@/src/ThreeHelper/decorators/DI";
// import { TransformControls } from "three/examples/jsm/controls/TransformControls";
import { TransformControls } from "@/src/ThreeHelper/addons/TransformControls";

@Injectable
export class Main extends MainScreen {
    static instance: Main;
    bones: THREE.Bone[] = [];
    boneIndex: number = -1;
    transformControls!: TransformControls;

    constructor(private helper: ThreeHelper) {
        super(helper);
        this.init();
        Main.instance = this;
    }

    @MethodBaseSceneSet({
        addAxis: false,
        cameraPosition: new THREE.Vector3(0, 1, 3),
        cameraTarget: new THREE.Vector3(0, 1, 0),
        near: 0.1,
        far: 100,
    })
    init() {
        this.initGui();
        this.loadModel();

        const transformControls = new TransformControls(
            this.helper.camera,
            this.helper.renderer.domElement
        );

        this.transformControls = transformControls;

        transformControls.mode = "rotate";

        this.helper.scene.add(transformControls);

        transformControls.addEventListener(
            "mouseDown",
            () => (this.helper.controls.enabled = false)
        );

        transformControls.addEventListener("mouseUp", () => {
            const { object } = transformControls;
            if (object && object.userData) {
                object.userData.updateTemplatePosition &&
                    object.userData.updateTemplatePosition();
            }
            this.helper.controls.enabled = true;
        });

        // translate
        // transformControls.ExplicitTrigger('XYZ');
        // rotate
        transformControls.ExplicitTrigger("XYZE");

        // this.helper.useSkyEnvironment();
        this.helper.setBackgroundDHR("/public/env/sunflowers_puresky_2k/");

    }

    @LoadGLTF("/public/models/michelle.glb")
    // @LoadGLTF("/public/models/observer1.gltf")
    // @LoadGLTF("/public/models/box.glb")
    loadModel(gltf?: GLTF) {
        if (gltf) {
            this.helper.add(gltf.scene);
            this.initBoneGui(gltf);

            const skeletonAnimation = new this.helper.SkeletonAnimation();
            skeletonAnimation.init(gltf.scene, gltf.animations);

            this.helper.gui?.addFunction(() => {
                skeletonAnimation.update();
            }, "播放骨骼动画");
            // requestAnimationFrame(() => {
            //     this.createMeshTemplate();
            // });
        }
    }

    @EventMesh.OnMouseDown(Main)
    handleMouseDown(
        backIntersection: BackIntersection,
        info: BackIntersection,
        event: MouseEvent
    ) {
        if (
            backIntersection &&
            backIntersection.faceIndex &&
            backIntersection.object?.geometry?.index
        ) {
            console.log(backIntersection.faceIndex);
            //* 通过 faceIndex 找到 geometry.index[index]  再找到 attributes.skinIndex[index] 就是骨骼的索引
            const skinIndex = backIntersection.object.geometry.index.getX(
                backIntersection.faceIndex * 3
            );
            const boneIndex =
                backIntersection.object.geometry.attributes.skinIndex.getX(
                    skinIndex
                );
            const bone = this.bones[boneIndex];

            console.log(bone);
            if (!bone) return;
            window.bone = bone;
            this.transformControls.attach(bone);
            this.transformControls.pointerDown(
                this.transformControls._getPointer(event)
            );
            EventMesh.enabledMouseMove = true;
            // if (bone) {
            //     this.bone = bone;
            //     this.boneIndex = boneIndex;
            //     this.generateIKStruct(
            //         <THREE.SkinnedMesh>(<unknown>backIntersection.object),
            //         bone,
            //         boneIndex
            //     );
            //     this.helper.controls.enabled = false;
            //     EventMesh.enabledMouseMove = true;
            // }
        }
    }

    @EventMesh.OnMouseMove(Main)
    handleMouseMove(event: MouseEvent) {
        this.transformControls.pointerMove(
            this.transformControls._getPointer(event)
        );
    }

    @EventMesh.OnMouseUp(Main)
    handleMouseUpControl(event: MouseEvent) {
        this.transformControls.pointerUp(
            this.transformControls._getPointer(event)
        );
        if (EventMesh.enabledMouseMove) {
            EventMesh.enabledMouseMove = false;
        }
    }

    initBoneGui(gltf: GLTF) {
        const skeleton = new THREE.SkeletonHelper(gltf!.scene);
        EventMesh.setIntersectObjects([gltf!.scene]);
        this.bones = skeleton.bones;
        const control = {
            addHelper:() => {
                this.helper.add(skeleton);
                this.helper.gui?.add(<any>skeleton, "visible").name("显示骨骼");
            }
        }
        this.helper.gui?.addFunction(control.addHelper).name("蒙皮骨骼");
        console.log(this.bones);
    }

    initGui() {
        const gui = this.helper.gui!;
        if (!gui) this.helper.addGUI();
        return gui;
    }

    @ThreeHelper.InjectAnimation(Main)
    animation() {}

    createMeshTemplate() {
        this.helper.scene.traverse((obj) => {
            if (obj.type == "SkinnedMesh") {
                const cloneMesh = this.helper.SkinnedToMesh(
                    <THREE.SkinnedMesh>obj
                );
                cloneMesh.visible = false
                // this.helper.add(cloneMesh);
                obj.parent?.add(cloneMesh);
                console.log(cloneMesh);
                window.cloneMesh = cloneMesh;
                // cloneMesh.position.x += 50;
                EventMesh.setIntersectObjects([cloneMesh]);
                // const {center,radius} = cloneMesh.geometry.boundingSphere
                // const {center,radius} = obj.geometry.boundingSphere
                // const {mesh} = this.helper.create.sphere(radius,32,32)
                // mesh.position.copy(center)

                // this.helper.add(mesh)
            }
        });
    }
}

覆盖Mesh的raycast方法

/*
 * @Author: hongbin
 * @Date: 2024-06-27 15:32:48
 * @LastEditors: hongbin
 * @LastEditTime: 2024-06-27 16:00:22
 * @Description:
 */
import * as THREE from "three";

const _ray = new THREE.Ray();
const _inverseMatrix = new THREE.Matrix4();

export class RayMesh extends THREE.Mesh {
    /**
     * 拦截Mesh的raycast方法 不检测 boundingSphere 配合SkinnedMesh转换的Mesh的射线使用
     */
    constructor(...arg: any[]) {
        super(...arg);
    }

    raycast(raycaster: THREE.Raycaster, intersects: any) {
        const geometry = this.geometry;
        const material = this.material;
        const matrixWorld = this.matrixWorld;

        if (material === undefined) return;

        _ray.copy(raycaster.ray).recast(raycaster.near);
        _inverseMatrix.copy(matrixWorld).invert();
        _ray.copy(raycaster.ray).applyMatrix4(_inverseMatrix);

        // test with bounding box in local space

        if (geometry.boundingBox !== null) {
            if (_ray.intersectsBox(geometry.boundingBox) === false) return;
        }

        // test for intersections with geometry

        super._computeIntersections(raycaster, intersects, _ray);
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值