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