实现
- 根据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');// rotatetransformControls.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 spaceif (geometry.boundingBox !== null) {if (_ray.intersectsBox(geometry.boundingBox) === false) return;}// test for intersections with geometrysuper._computeIntersections(raycaster, intersects, _ray);}
}