我解决这个问题的方法是在 2D 平面上映射零点和法线点,然后使用反斜率找到与法线相交的垂直线。然后我可以使用起点和交点来找到鼠标移动的距离。我还必须使用相机缩放最终距离。
快速参考:
// linear slope/intercept: y = mx + b
// solve for b: b = y - mx
// solve for m: (y2 - y1) / (x2 - x1)
// get inverse slope: -1 / m
// get intersect point: (b2 - b1) / (m1 - m2)
可能有更简单的方法,但这是我所做的,希望它可以帮助其他人:
鼠标按下时
-
将中心(0,0,0)向量、人脸法线向量和任意1单位向量(1,0,0)投影到相机上,得到三个点的屏幕位置
var zero2D = toScreenPosition(0, 0, 0);
var one2D = toScreenPosition(1, 0, 0);
var normal2D = toScreenPosition(intersect.face.normal.x, intersect.face.normal.y, intersect.face.normal.z);
/ ***** /
var toScreenPosition = function (x, y, z) {
var rect = viewport.getBoundingClientRect();
var point = new THREE.Vector2();
screenPositionVector.set(x || 0, y || 0, z || 0);
screenPositionVector.project(camera);
point.set((screenPositionVector.x + 1) / 2 * rect.width, -(screenPositionVector.y - 1) / 2 * rect.height);
return point;
};
-
存储鼠标起点和法线的x方向(1或-1)。
start2D.set(event.clientX, event.clientY);
normalDir = zero2D.x < normal2D.x ? 1 : -1;
-
存储零/法线的斜率和反斜率。
slope = (normal2D.y - zero2D.y) / (normal2D.x - zero2D.x); // TODO: Handle zero slope
inverseSlope = -1 / slope; // TODO: If slope is 0, inverse is infinity
-
根据鼠标坐标存储法线的y截距。
startingYIntercept = event.clientY - (slope * event.clientX);
-
使用 zero2D 和 one2D 点查找相机比例。相机比例是两个 2D 点之间的距离除以两个 3D 点之间的距离 (1)。
cameraScale = one2D.distanceTo(zero2D);
-
为了提高准确性,我们将根据总移动量来移动顶点,而不是事件处理程序调用之间的增量。因此,我们需要跟踪所有顶点的起始位置。
startingVertices = [];
var i;
for (i = 0; i < vertices.length; i++) {
startingVertices.push({x: vertices[i].x, y: vertices[i].y, z: vertices[i].z});
}
鼠标移动
-
使用鼠标位置和反斜率,找到垂直线的 y 截距。
var endingYIntercept = event.clientY - (inverseSlope * event.clientX);
-
使用截距方程求法线和垂线相交的x位置。
var endingX = (endingYIntercept - startingYIntercept) / (slope / inverseSlope);
-
重新插入 x 以找到 y 点。由于线在 x 处相交,因此您可以使用法线或垂线。在此基础上设置终点。
var endingY = (slope * endingX) + startingYIntercept;
end2D.set(endingX, endingY);
-
找出点之间的距离并除以相机比例。
var distance = end2D.distanceTo(start2D) / cameraScale;
-
如果法线与鼠标移动方向相反,则将距离乘以-1。
if ((normalDir > 0 && endingX < start2D.x) || (normalDir < 0 && endingX > start2D.x)) {
distance = distance * -1;
}
-
由于我们将顶点移动总距离而不是事件处理程序之间的增量,因此顶点更新代码略有不同。
var i;
for (i = 0; i < self.vertices.length; i++) {
vertices[i].x = startingVertices[i].x + (normal.x * distance);
vertices[i].y = startingVertices[i].y + (normal.y * distance);
vertices[i].z = startingVertices[i].z + (normal.z * distance);
}
Mouseup 额外积分
-
移动顶点时,几何体的中心不会改变,需要更新。要更新中心,我可以调用 geometry.center(),但是,在 Three.js 中,几何的位置基于其中心,因此这将有效地移动中心和几何的位置在顶点移动一半距离的相反方向上。我不希望这样,我希望几何图形在移动顶点时保持在同一位置。为此,我将第一个顶点的结束位置减去其开始位置除以 2,然后将该向量添加到几何体的位置。然后我重新定位几何。
if (_dragging && self.vertices.length > 0) {
offset.set(self.vertices[0].x - startingVertices[0].x, self.vertices[0].y - startingVertices[0].y, self.vertices[0].z - startingVertices[0].z);
offset.divideScalar(2);
object.position.add(offset);
object.geometry.center();
}
齐心协力
var object; // Set outside this code
var camera; // Set outside this code
var viewport; // Set outside this code
var raycaster = new THREE.Raycaster();
var point = new THREE.Vector2();
var mouse = new THREE.Vector2();
var _dragging = false;
var faces = [];
var vertices = [];
var startingVertices = [];
var slope = 0;
var inverseSlope;
var startingYIntercept = 0;
var normalDir = 1;
var cameraScale = 1;
var start2D = new THREE.Vector2();
var end2D = new THREE.Vector2();
var offset = new THREE.Vector3();
var onMouseDown = function (event) {
if (object === undefined || _dragging === true) {
return;
}
event.preventDefault();
event.stopPropagation();
var intersect = getIntersects(event, object)[0];
if (intersect && intersect.face) {
var zero2D = toScreenPosition(0, 0, 0);
var one2D = toScreenPosition(1, 0, 0);
var normal2D = toScreenPosition(intersect.face.normal.x, intersect.face.normal.y, intersect.face.normal.z);
start2D.set(event.clientX, event.clientY);
normalDir = zero2D.x < normal2D.x ? 1 : -1;
slope = (normal2D.y - zero2D.y) / (normal2D.x - zero2D.x); // TODO: Handle zero slope
inverseSlope = -1 / slope; // TODO: If slope is 0, inverse is infinity
startingYIntercept = event.clientY - (slope * event.clientX);
cameraScale = one2D.distanceTo(zero2D);
faces = getAdjacentNormalFaces(intersect.object.geometry, intersect.face);
vertices = getFaceVertices(intersect.object.geometry, self.faces);
startingVertices = [];
var i;
for (i = 0; i < vertices.length; i++) {
startingVertices.push({x: vertices[i].x, y: vertices[i].y, z: vertices[i].z});
}
}
_dragging = true;
}
var onMouseMove = function (event) {
if (object === undefined || vertices.length === 0 || _dragging === false) {
return;
}
event.preventDefault();
event.stopPropagation();
var normal = faces[0].normal;
var endingYIntercept = event.clientY - (inverseSlope * event.clientX);
var endingX = (endingYIntercept - startingYIntercept) / (slope / inverseSlope);
var endingY = (slope * endingX) + startingYIntercept;
end2D.set(endingX, endingY);
var distance = end2D.distanceTo(start2D) / cameraScale;
if ((normalDir > 0 && endingX < start2D.x) || (normalDir < 0 && endingX > start2D.x)) {
distance = distance * -1;
}
var i;
for (i = 0; i < self.vertices.length; i++) {
vertices[i].x = startingVertices[i].x + (normal.x * distance);
vertices[i].y = startingVertices[i].y + (normal.y * distance);
vertices[i].z = startingVertices[i].z + (normal.z * distance);
}
object.geometry.verticesNeedUpdate = true;
object.geometry.computeBoundingBox();
object.geometry.computeBoundingSphere();
}
var onMouseUp = function (event) {
if (_dragging && vertices.length > 0) {
offset.set(vertices[0].x - startingVertices[0].x, vertices[0].y - startingVertices[0].y, vertices[0].z - startingVertices[0].z);
offset.divideScalar(2);
object.position.add(offset);
object.geometry.center();
}
}
var getIntersects = function (event, object) {
var rect = viewport.getBoundingClientRect();
point.fromArray([
( event.clientX - rect.left ) / rect.width,
( event.clientY - rect.top ) / rect.height
]);
mouse.set(( point.x * 2 ) - 1, -( point.y * 2 ) + 1);
raycaster.setFromCamera(mouse, camera);
if (object instanceof Array) {
return raycaster.intersectObjects(object);
}
return raycaster.intersectObject(object);
};
var toScreenPosition = function (x, y, z) {
var rect = viewport.getBoundingClientRect();
var point = new THREE.Vector2();
screenPositionVector.set(x || 0, y || 0, z || 0);
screenPositionVector.project(camera);
point.set((screenPositionVector.x + 1) / 2 * rect.width, -(screenPositionVector.y - 1) / 2 * rect.height);
return point;
};
var getAdjacentNormalFaces = function (geometry, face) {
// Returns an array of all faces that are adjacent and share the same normal vector
};
var getFaceVertices = function (geometry, faces) {
// Returns an array of vertices that belong to the array of faces
};