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

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

实现

  • 根据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);
    }
}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/750704.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

马面裙的故事:汉服如何通过直播电商实现产业跃迁

【潮汐商业评论/原创】 波澜壮阔的千里江山在马面裙的百褶上展开&#xff0c;织金花纹在女性的步伐之间若隐若现&#xff0c;从明清到现代&#xff0c;如今马面裙又流行了回来&#xff0c;成为女性的流行单品&#xff0c;2024年春节期间&#xff0c;马面裙更是成为华夏女孩们的…

仓库管理系统14--仓库设置

1、添加窗体 <UserControl x:Class"West.StoreMgr.View.StoreView"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc"http://schemas.openxmlformats.…

Str.format()方法

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 语法参考 在Python2.6之后&#xff0c;提供了字符串的format()方法对字符串进行格式化操作。format()功能非常强大&#xff0c;格式也比较复杂&…

选择第三方软件测试机构做验收测试的好处简析

企事业单位在自行开发完软件系统或委托软件开发公司生产软件之后&#xff0c;有一个必经流程就是验收测试&#xff0c;以验证该产品是否符合用户需求、是否可以上线。为了客观评估所委托生产的软件质量&#xff0c;第三方软件测试机构往往成为企事业单位做验收测试的首选&#…

Bad owner or permissions on C:\\Users\\username/.ssh/config > 过程试图写入的管道不存在。

使用windows连接远程服务器出现Bad owner or permissions 错误 问题&#xff1a; 需要修复文件权限 SSH 配置文件应具有受限权限以防止未经授权的访问 确保只有用户对该.ssh/config文件具有读取权限 解决方案&#xff1a; 在windows下打开命令行&#xff0c;通过以下命令打开文…

PS使用批量脚本生成海报实践

前言 设计朋友有需求做一批邀请函&#xff0c;有几十个人名&#xff0c;需要把人名加到海报中&#xff0c;PS里一个一个添加人名很麻烦&#xff0c;于是来问我有没有什么办法能够批量去添加。 希望把人名加到红框区域内 尝试用ps的脚本进行处理 准备 PS(版本2021&#xff0c;…

HTML静态网页成品作业(HTML+CSS)——企业摄影网介绍网页(3个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有3个页面。 二、作品演示 三、代…

Micro-ROS是什么?

Micro-ROS是ROS&#xff08;Robot Operating System&#xff0c;机器人操作系统&#xff09;生态系统的一个重要组成部分&#xff0c;专为微控制器&#xff08;Microcontrollers&#xff09;设计的轻量级ROS版本。它的目标是在资源有限的嵌入式平台上实现ROS 2的功能&#xff0…

各省药品集中采购平台-地方药品集采分析数据库

国家第十批药品集中采购的启动时间暂未明确&#xff0c;但即将到来&#xff0c;在5月&#xff0c;国家医保局发布了《关于加强区域协同做好2024年医药集中采购提质扩面的通知》&#xff0c;其中明确指出将“开展新批次国家组织药品和医用耗材集中带量采购&#xff0c;对协议期满…

转转游戏MQ重构:思考与心得之旅

文章目录 1 背景1.1 起始之由1.2 重构前现状1.3 问题分析 2 重构2.1 目标2.2 制定方案2.2.1 架构设计2.2.2 实施计划2.2.3 测试计划 2.3 部分细节设计 3. 总结 1 背景 游戏业务自 2017 年启航&#xff0c;至今已近乎走过七个春秋&#xff0c;历经漫长岁月的发展&#xff0c;不…

应用图扑 HT for Web 搭建拓扑关系图

拓扑结构在计算机网络设计和通信领域中非常重要&#xff0c;因为它描述了网络中的设备&#xff08;即“点”&#xff09;如何相互连接&#xff08;即通过“线”&#xff09;。这种结构不仅涉及物理布局&#xff0c;即物理拓扑&#xff0c;还可以涉及逻辑或虚拟的连接方式&#…

C++ ─── vector模拟实现的扩容拷贝问题

扩容拷贝问题 源代码使用memcpy拷贝&#xff0c;在使用vector<int>存储内置类型时没有问题&#xff0c; 但是如果存储的是含有指针的类型&#xff0c;如string&#xff0c;就会发生浅拷贝问题 //3、容量相关void reserve(size_t n){if (n > capacity()){size_t old_si…

数字水产养殖中的鱼类追踪、计数和行为分析技术

随着全球人口增长和生态环境退化&#xff0c;传统捕捞已无法满足人类对水产品的需求&#xff0c;水产养殖成为主要的鱼类来源。数字水产养殖利用先进技术和数据驱动方法&#xff0c;对提高生产效率、改善鱼类福利和资源管理具有显著优势。 1 数字水产养殖的重要性 1.1 提高生…

Java web应用性能分析之【prometheus监控指标体系】

Java web应用性能分析之【系统监控工具prometheus】_javaweb服务器性能监控工具-CSDN博客 Java web应用性能分析之【prometheusGrafana监控springboot服务和服务器监控】_grafana 导入 prometheus-CSDN博客 因为篇幅原因&#xff0c;前面没有详细说明Prometheus的监控指标&…

小红书2024LLM论文分享

2024小红书大模型论文分享 BatchEval基于LLM评估LLM生成文本的质量 ACL2024 https://ypw0102.github.io/ 如果文本评价需要多个维度&#xff0c;需要调整BatchEval么&#xff1f; 目前是完整流程走一遍的&#xff0c;因此没有具体考虑细粒度。 评测连续的数据域&#xff0c;S…

使用飞书多维表格实现推送邮件

一、为什么用飞书&#xff1f; 在当今竞争激烈的商业环境中&#xff0c;选择一款高效、智能的办公工具至关重要。了解飞书的朋友应该都知道&#xff0c;飞书的集成能力是很强大的&#xff0c;能够与各种主流的办公软件无缝衔接&#xff0c;实现数据交互&#xff0c;提升工作效…

VAE-pytorch代码

import osimport torch import torch.nn as nn import torch.nn.functional as F from torch.utils.data import DataLoaderfrom torchvision import transforms, datasets from torchvision.utils import save_imagefrom tqdm import tqdmclass VAE(nn.Module): # 定义VAE模型…

基于盲信号处理的声音分离-基于改进的信息最大化的ICA算法

基于信息最大化的ICA算法的主要依据是使输入端与输出端的互信息达到最大&#xff0c;且输出各个分量之间的相关性最小化&#xff0c;即输出各个分量之间互信息量最小化&#xff0c;其算法的系统框图如图所示。 基于信息最大化的ICA算法的主要依据是使输入端与输出端的互信息达到…

java基于ssm+jsp 弹幕视频网站

1前台首页功能模块 弹幕视频网站&#xff0c;在弹幕视频网站可以查看首页、视频信息、商品信息、论坛信息、我的、跳转到后台、购物车、客服等内容&#xff0c;如图1所示。 图1前台首页界面图 登录&#xff0c;通过登录填写账号、密码等信息进行登录操作&#xff0c;如图2所示…

Sparse4D v1

Sparse4D: Multi-view 3D Object Detection with Sparse Spatial-Temporal Fusion 单位&#xff1a;地平线 GitHub&#xff1a;https://github.com/HorizonRobotics/Sparse4D 论文&#xff1a;https://arxiv.org/abs/2211.10581 时间&#xff1a;2022-11 找博主项目讨论方…