three.js中如何将Vector3转换为旋转场景下的2D屏幕坐标?

3
我想把一个div放在一个物体的最高点,稍微向左上方偏移一点,以免直接覆盖顶点。你可以在这里看到。如果jsfiddle链接不再有效,请查看下面的不使用场景旋转的工作示例片段。我可以告诉你,它的效果非常好。
然而,在工作中的一个项目中,我需要旋转场景本身。显然,这会破坏将其转换为2D屏幕坐标。您可以在这里查看。您还可以在下面查看不适用于场景旋转的示例片段。正如您所看到的,该标签在垂直(“y-”)方向上未更新,但在“水平”方向上确实更新了。“这是因为相机的位置确实围绕x-z平面(phi改变)发生了变化,但它从不改变y位置(theta从不改变),而是当鼠标拖动时旋转场景。正如我所写的,我需要旋转场景来实现所需的效果。
如果有人能指导我正确的方向或在任何html/js/css片段网站(如jsfiddle等)上进行快速示例,他将是个救星!
附注:我尝试过(显然没有成功)通过将每个顶点的x和y坐标转换为“正确旋转”的位置来跳跃(即通过乘以sin(sceneRotation)cos(sceneRotation)),但这只会使情况更糟。
另外注意:我还花了一个小时旋转场景中的每个对象,但由于对象实际上存储在THREE.Group中,所以其效果完全相同。
工作示例(不使用场景旋转)
点击下面的运行片段

var scene = new THREE.Scene();
var w = window.innerWidth, h = window.innerHeight;
var camera = new THREE.PerspectiveCamera(60, w / h, 1, 1000);
camera.position.set(0, 0, -60);
var renderer = new THREE.WebGLRenderer({
    alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.domElement.style.backgroundColor = "#bbbbbb"
document.body.appendChild(renderer.domElement);

var label = document.getElementById("label");
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.addEventListener("change", updateLabel);

var geom = new THREE.Geometry();
geom.vertices.push(new THREE.Vector3(-10, 10, -10));
geom.vertices.push(new THREE.Vector3(10, 10, -10));
geom.vertices.push(new THREE.Vector3(10, -10, -10));
geom.vertices.push(new THREE.Vector3(-10, -10, -10));
geom.vertices.push(new THREE.Vector3(-10, 10, 10));
geom.vertices.push(new THREE.Vector3(10, 10, 10));
geom.vertices.push(new THREE.Vector3(10, -10, 10));
geom.vertices.push(new THREE.Vector3(-10, -10, 10));
geom.faces.push(new THREE.Face3(0, 1, 2));
geom.faces.push(new THREE.Face3(0, 2, 3));
geom.faces.push(new THREE.Face3(7, 6, 5));
geom.faces.push(new THREE.Face3(7, 5, 4));
geom.faces.push(new THREE.Face3(4, 5, 1));
geom.faces.push(new THREE.Face3(4, 1, 0));
geom.faces.push(new THREE.Face3(3, 2, 6));
geom.faces.push(new THREE.Face3(3, 6, 7));
geom.faces.push(new THREE.Face3(4, 0, 3));
geom.faces.push(new THREE.Face3(4, 3, 7));
geom.faces.push(new THREE.Face3(1, 5, 6));
geom.faces.push(new THREE.Face3(1, 6, 2));
var mat = new THREE.MeshBasicMaterial({ color: 0x3377dd, transparent: true, opacity: .65, wireframe: false });
var cube = new THREE.Mesh(geom, mat);
scene.add(cube);
var matWire = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true });
var cube1 = new THREE.Mesh(geom, matWire);
scene.add(cube1);

render();

function render() {
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}

function getScreenPosition(position) {
    var vector = new THREE.Vector3( position.x, position.y, position.z );

    vector.project(camera);

    vector.x = Math.round( (   vector.x + 1 ) * w / 2 );
    vector.y = Math.round( ( - vector.y + 1 ) * h / 2 );

    return vector;
}

function updateLabel() {
    var minY = null, x = null,
     verts = cube.geometry.vertices;
    for (var i = 0, iLen = verts.length; i < iLen; i++) {
        var pos = getScreenPosition(verts[i]);
        if (minY === null || pos.y < minY) {
         minY = pos.y;
            x = pos.x;
        }
    }
    label.style.left = (x - 3) + "px";
    label.style.top = (minY - 28) + "px";
}
body {
    overflow: hidden;
    margin: 0;
}

#label {
    position: absolute;
    left: 20px; top: 20px;
}
<div id="label">label</div>
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

无法与场景旋转配合使用

点击下方的运行片段

/* "globals" */
/* ~~~~~~~~~ */
var PI = Math.PI;
/* camera stuff */
var lastX, lastY, r = 60, phi = 0, c = new THREE.Vector3(0, 0, 0);
/* three.js stuff */
var scene = new THREE.Scene();
var w = window.innerWidth, h = window.innerHeight;
var camera = new THREE.PerspectiveCamera(60, w / h, 1, 1000);
//camera.position.set(0, 0, -60);
updateCamera();
var renderer = new THREE.WebGLRenderer({
    alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.domElement.style.backgroundColor = "#bbbbbb"
document.body.appendChild(renderer.domElement);

var label = document.getElementById("label");
//var controls = new THREE.OrbitControls(camera, renderer.domElement);
//controls.addEventListener("change", updateLabel);
document.body.addEventListener("mousedown", handleMouseDown);
document.body.addEventListener("touchstart", handleTouchStart);

var geom = new THREE.Geometry();
geom.vertices.push(new THREE.Vector3(-10, 10, -10));
geom.vertices.push(new THREE.Vector3(10, 10, -10));
geom.vertices.push(new THREE.Vector3(10, -10, -10));
geom.vertices.push(new THREE.Vector3(-10, -10, -10));
geom.vertices.push(new THREE.Vector3(-10, 10, 10));
geom.vertices.push(new THREE.Vector3(10, 10, 10));
geom.vertices.push(new THREE.Vector3(10, -10, 10));
geom.vertices.push(new THREE.Vector3(-10, -10, 10));
geom.faces.push(new THREE.Face3(0, 1, 2));
geom.faces.push(new THREE.Face3(0, 2, 3));
geom.faces.push(new THREE.Face3(7, 6, 5));
geom.faces.push(new THREE.Face3(7, 5, 4));
geom.faces.push(new THREE.Face3(4, 5, 1));
geom.faces.push(new THREE.Face3(4, 1, 0));
geom.faces.push(new THREE.Face3(3, 2, 6));
geom.faces.push(new THREE.Face3(3, 6, 7));
geom.faces.push(new THREE.Face3(4, 0, 3));
geom.faces.push(new THREE.Face3(4, 3, 7));
geom.faces.push(new THREE.Face3(1, 5, 6));
geom.faces.push(new THREE.Face3(1, 6, 2));
var mat = new THREE.MeshBasicMaterial({ color: 0x3377dd, transparent: true, opacity: .65, wireframe: false });
var cube = new THREE.Mesh(geom, mat);
//cube.translateX(10);
scene.add(cube);
var matWire = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true });
var cube1 = new THREE.Mesh(geom, matWire);
//cube1.translateX(10);
scene.add(cube1);

render();

function render() {
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}

function getScreenPosition(position) {
    var vector = new THREE.Vector3( position.x, position.y, position.z );

    vector.project(camera);

    vector.x = Math.round( (   vector.x + 1 ) * w / 2 );
    vector.y = Math.round( ( - vector.y + 1 ) * h / 2 );

    return vector;
}

function updateLabel() {
    var minY = null, x = null,
     verts = cube.geometry.vertices;
    for (var i = 0, iLen = verts.length; i < iLen; i++) {
        var pos = getScreenPosition(verts[i]);
        if (minY === null || pos.y < minY) {
         minY = pos.y;
            x = pos.x;
        }
    }
    label.style.left = (x - 3) + "px";
    label.style.top = (minY - 28) + "px";
}

function handleMouseDown(ev) {
    ev.preventDefault();
    mouseOrTouchDown(ev.pageX, ev.pageY);
}

function handleTouchStart(ev) {
    var touches = ev.touches;
    if (touches.length !== 1) {
        return;
    }
    ev.preventDefault();
    mouseOrTouchDown(touches.item(0).pageX, touches.item(0).pageY, true);
}

function mouseOrTouchDown(downX, downY, touch) {
    if (touch === undefined) { touch = false; }
    lastX = downX;
    lastY = downY;
    if (touch) {
        document.ontouchmove = handleTouchMove;
        document.addEventListener("touchend", function(ev) {
            document.ontouchmove = null;
        });
        document.addEventListener("touchcancel", function(ev) {
            document.removeEventListener("touchmove", handleTouchMove);
        });
    } else {
        document.addEventListener("mousemove", handleMouseMove);
        document.addEventListener("mouseup", function(ev) {
            document.removeEventListener("mousemove", handleMouseMove);
        });
    }
}

function handleMouseMove(ev) {
    ev.preventDefault();
    mouseOrTouchMove(ev.pageX, ev.pageY);
}

function handleTouchMove(ev) {
    var touches = ev.touches;
    if (touches.length !== 1) {
        return;
    }
    ev.preventDefault();
    mouseOrTouchMove(touches.item(0).pageX, touches.item(0).pageY);
}

function mouseOrTouchMove(x, y) {
    var dx = lastX - x, dy = y - lastY; /* change in {x, y} */

    phi -= dx / 100;
    if (phi > 2 * PI) {
        phi -= 2 * PI;
    } else if (phi < 0) {
        phi += 2 * PI;
    }

    if (phi < PI / 2 || phi > 3 * PI / 2) {
        sign = -1;
    } else {
        sign = 1;
    }
    if (scene.rotation.z + sign * dy / 100 < -PI) {
        scene.rotation.z = -PI;
    } else if (scene.rotation.z + sign * dy / 100 > 0) {
        scene.rotation.z = 0;
    } else {
        scene.rotateZ(sign * dy / 100);
    }

    lastX = x;
    lastY = y;

    updateCamera();
    updateLabel();
}

function updateCamera() {
    var z = r * Math.sin(phi); /* new y pos (z-axis) in x-z plane */
    var x = r * Math.cos(phi); /* new x pos (x-axis) in x-z plane */
    camera.position.set(x, 1, z);
    camera.lookAt(c);
}
body {
    overflow: hidden;
    margin: 0;
}

#label {
    position: absolute;
    left: 20px; top: 20px;
}
<div id="label">label</div>
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>


你通过旋转场景想要达到什么效果,而通过旋转相机却无法实现呢? - TheJim01
@TheJim01 我已经与我的同事商量过了,也考虑了一下。不幸的是,我不是欧拉,所以我很难想出如何设置相机的旋转,以便我可以获得所需的效果。当我向左或向右拖动时,我需要相机在x-z平面上旋转(phi,你可能知道),当我向上或向下拖动时,我需要场景绕特定轴(比如说x轴)旋转(或看起来像旋转)。这就是我需要做的。万向锁或欧拉角或其他东西让我感到很困扰。也许你知道我不知道的东西! - dylnmc
本质上,如果我没有按照我现在的方式(旋转场景或场景中的所有对象)来做,那么我就必须找到相机相对于场景中心的位置,并相应地旋转它...这是一些我不太熟悉的难度为3D计算 >_< 另外,感谢您迅速的回复!^_^ - dylnmc
2个回答

4
在渲染中,场景中的每个网格通常都是通过模型矩阵、视图矩阵和投影矩阵进行变换的。
模型矩阵: 模型矩阵定义了场景中一个网格的位置、方向和相对大小。模型矩阵将网格的顶点位置从模型空间转换到世界空间。
视图矩阵: 视图矩阵描述了观察场景时的方向和位置。视图矩阵将世界空间转换为视图(眼睛)空间。在视口的坐标系中,X轴指向左,Y轴指向上,Z轴指向视图外(注意,在右手坐标系中,Z轴是X轴和Y轴的叉积)。
投影矩阵: 投影矩阵描述了场景中3D点到视口2D点的映射关系。投影矩阵将视图空间转换为裁剪空间,并将裁剪空间中的坐标通过除以裁剪坐标的w分量来转换为范围为(-1,-1,-1)到(1,1,1)的归一化设备坐标(NDC)。
如果您想知道几何体上的一个点在视口上的位置,则必须执行所有这些变换,并将其从归一化设备坐标(NDC)转换为窗口坐标(像素)。
视图矩阵和投影矩阵的变换由“project”执行。
vector.project(camera);

将标准化设备坐标(NDC)转换为窗口坐标(像素)的方法如下所示:
vector.x = Math.round( (   vector.x + 1 ) * w / 2 );
vector.y = Math.round( ( - vector.y + 1 ) * h / 2 );

您忘记了使用模型矩阵进行变换,可以通过以下方式完成:

您忘记了使用模型矩阵进行变换,可以通过以下方式完成:

var modelMat = cube.matrixWorld;
vector.applyMatrix4(modelMat);

    // world to view and view to NDC
    vector.project(camera);

    // NDC to pixel
    vector.x = Math.round( (   vector.x + 1 ) * w / 2 );
    vector.y = Math.round( ( - vector.y + 1 ) * h / 2 );
   
    return vector;
}

/* can't see top line; thanks jsfiddle */
    /* "globals" */
    /* ~~~~~~~~~ */
    var PI = Math.PI;
    /* camera stuff */
    var lastX, lastY, r = 60, phi = 0, c = new THREE.Vector3(0, 0, 0);
    /* three.js stuff */
    var scene = new THREE.Scene();
    var w = window.innerWidth, h = window.innerHeight;
    var camera = new THREE.PerspectiveCamera(60, w / h, 1, 1000);
    //camera.position.set(0, 0, -60);
    updateCamera();
    var renderer = new THREE.WebGLRenderer({
        alpha: true
    });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.domElement.style.backgroundColor = "#bbbbbb"
    document.body.appendChild(renderer.domElement);

    var label = document.getElementById("label");
    //var controls = new THREE.OrbitControls(camera, renderer.domElement);
    //controls.addEventListener("change", updateLabel);
    document.body.addEventListener("mousedown", handleMouseDown);
    document.body.addEventListener("touchstart", handleTouchStart);

    var geom = new THREE.Geometry();
    geom.vertices.push(new THREE.Vector3(-10, 10, -10));
    geom.vertices.push(new THREE.Vector3(10, 10, -10));
    geom.vertices.push(new THREE.Vector3(10, -10, -10));
    geom.vertices.push(new THREE.Vector3(-10, -10, -10));
    geom.vertices.push(new THREE.Vector3(-10, 10, 10));
    geom.vertices.push(new THREE.Vector3(10, 10, 10));
    geom.vertices.push(new THREE.Vector3(10, -10, 10));
    geom.vertices.push(new THREE.Vector3(-10, -10, 10));
    geom.faces.push(new THREE.Face3(0, 1, 2));
    geom.faces.push(new THREE.Face3(0, 2, 3));
    geom.faces.push(new THREE.Face3(7, 6, 5));
    geom.faces.push(new THREE.Face3(7, 5, 4));
    geom.faces.push(new THREE.Face3(4, 5, 1));
    geom.faces.push(new THREE.Face3(4, 1, 0));
    geom.faces.push(new THREE.Face3(3, 2, 6));
    geom.faces.push(new THREE.Face3(3, 6, 7));
    geom.faces.push(new THREE.Face3(4, 0, 3));
    geom.faces.push(new THREE.Face3(4, 3, 7));
    geom.faces.push(new THREE.Face3(1, 5, 6));
    geom.faces.push(new THREE.Face3(1, 6, 2));
    var mat = new THREE.MeshBasicMaterial({ color: 0x3377dd, transparent: true, opacity: .65, wireframe: false });
    var cube = new THREE.Mesh(geom, mat);
    //cube.translateX(10);
    scene.add(cube);
    var matWire = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true });
    var cube1 = new THREE.Mesh(geom, matWire);
    //cube1.translateX(10);
    scene.add(cube1);

    render();

    function render() {
        requestAnimationFrame(render);
        renderer.render(scene, camera);
    }

    function getScreenPosition(position, object) {
        var vector = new THREE.Vector3( position.x, position.y, position.z );

        // model to world
        if ( object != null ) {
            var modelMat = cube.matrixWorld;
            vector.applyMatrix4(modelMat);
        }

        // world to view and view to NDC
        vector.project(camera);

        // NDC to pixel
        vector.x = Math.round( (   vector.x + 1 ) * w / 2 );
        vector.y = Math.round( ( - vector.y + 1 ) * h / 2 );

        return vector;
    }

    function updateLabel() {
        var minY = null, x = null,
            verts = cube.geometry.vertices;
        for (var i = 0, iLen = verts.length; i < iLen; i++) {
            var pos = getScreenPosition(verts[i], cube);
            if (minY === null || pos.y < minY) {
                minY = pos.y;
                x = pos.x;
            }
        }
        label.style.left = (x - 3) + "px";
        label.style.top = (minY - 28) + "px";
    }

    function handleMouseDown(ev) {
        ev.preventDefault();
        mouseOrTouchDown(ev.pageX, ev.pageY);
    }

    function handleTouchStart(ev) {
        var touches = ev.touches;
        if (touches.length !== 1) {
            return;
        }
        ev.preventDefault();
        mouseOrTouchDown(touches.item(0).pageX, touches.item(0).pageY, true);
    }

    function mouseOrTouchDown(downX, downY, touch) {
        if (touch === undefined) { touch = false; }
        lastX = downX;
        lastY = downY;
        if (touch) {
            document.ontouchmove = handleTouchMove;
            document.addEventListener("touchend", function(ev) {
                document.ontouchmove = null;
            });
            document.addEventListener("touchcancel", function(ev) {
                document.removeEventListener("touchmove", handleTouchMove);
            });
        } else {
            document.addEventListener("mousemove", handleMouseMove);
            document.addEventListener("mouseup", function(ev) {
                document.removeEventListener("mousemove", handleMouseMove);
            });
        }
    }

    function handleMouseMove(ev) {
        ev.preventDefault();
        mouseOrTouchMove(ev.pageX, ev.pageY);
    }

    function handleTouchMove(ev) {
        var touches = ev.touches;
        if (touches.length !== 1) {
            return;
        }
        ev.preventDefault();
        mouseOrTouchMove(touches.item(0).pageX, touches.item(0).pageY);
    }

    function mouseOrTouchMove(x, y) {
        var dx = lastX - x, dy = y - lastY; /* change in {x, y} */

        phi -= dx / 100;
        if (phi > 2 * PI) {
            phi -= 2 * PI;
        } else if (phi < 0) {
            phi += 2 * PI;
        }

        if (phi < PI / 2 || phi > 3 * PI / 2) {
            sign = -1;
        } else {
            sign = 1;
        }
        if (scene.rotation.z + sign * dy / 100 < -PI) {
            scene.rotation.z = -PI;
        } else if (scene.rotation.z + sign * dy / 100 > 0) {
            scene.rotation.z = 0;
        } else {
            scene.rotateZ(sign * dy / 100);
        }

        lastX = x;
        lastY = y;

        updateCamera();
        updateLabel();
    }

    function updateCamera() {
        var z = r * Math.sin(phi); /* new y pos (z-axis) in x-z plane */
        var x = r * Math.cos(phi); /* new x pos (x-axis) in x-z plane */
        camera.position.set(x, 1, z);
        camera.lookAt(c);
    }
body {
    overflow: hidden;
    margin: 0;
}

#label {
    position: absolute;
    left: 20px; top: 20px;
}
<div id="label">label</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.116/build/three.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.116/examples/js/controls/OrbitControls.js"></script>


1
太棒了!感谢你在最后提供的代码片段。看起来没问题!我回到工作岗位后会尝试实现它! - dylnmc
在我的实际应用中还有一些其他因素使这变得更加复杂,但这确实让我走上了正确的轨道。再次感谢,就像我说的,“救命稻草”!:D - dylnmc

0

不知道是否还有人对这个问题感兴趣,但解决方案有点过度。它基本上重新实现了一个裸骨轨道控制器,这对教育目的来说很好,但如果您想要轨道控制器的其他功能,那么事情会变得有点混乱。

问题不在于轨道控制器“将转换为2D屏幕坐标搞砸了”。它只是没有更新相机矩阵。如果设置为渲染,则这些矩阵会自动更新,但是绘制的内容将与同步不一致。

解决方案是必须显式地更新矩阵。不仅如此,您还必须在需要进行任何更改时立即执行此操作。关键词是更改,因为轨道控制器有一个名为“change”的事件侦听器。因此,您所要做的就是在回调函数中更新矩阵,然后执行所需的任何计算。例如更新此标签。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接