javascript - three.js Vector3 到 2D 屏幕坐标与旋转场景

标签 javascript 3d three.js coordinate-transformation

我试图定位一个 div,使其始终位于对象的最高点,向左和向上偏移一点,因此它不在顶点的正上方。你可以看到 here .如果 jsfiddle 链接不再有效,请参阅下面的在没有场景旋转的情况下工作 片段。我可以告诉你,它的效果非常好。

但是,对于工作中的项目,我需要旋转场景本身。显然,这搞乱了到 2D 屏幕坐标的转换。你可以查看这个here .您还可以查看下面的不使用场景旋转 片段。如您所见,标签不会在垂直(“y-”)方向更新,但会“水平”更新。这是因为相机的位置确实在 x-z 平面周围发生变化(phi 变化),但它永远不会改变其 y 位置(theta 永远不会改变)——而是当鼠标向上/向下拖动时场景会旋转。如我所写,我需要旋转场景以获得所需的效果。

如果有人能为我指出正确的方向或在任何 html/js/css 片段网站(如 jsfiddle 等)中快速举例,{,s}他将成为救星!

  • 旁注:我尝试(显然没有成功)跳过一些步骤将每个顶点的 x 和 y 坐标转换为“正确旋转”位置(即乘以 sin(sceneRotation)cos(sceneRotation),但这只会让情况变得更糟。

  • 旁注 2:我还花了一个小时旋转场景中的每个对象,但由于这些对象实际上存储在 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>

最佳答案

在渲染中,场景的每个网格通常由模型矩阵、 View 矩阵和投影矩阵进行变换。

  • 模型矩阵:
    模型矩阵定义场景中网格的位置、方向和相对大小。模型矩阵将顶点位置从网格变换到世界空间。

  • 查看矩阵:
    View 矩阵描述了观察场景的方向和位置。 View 矩阵从世界空间转换到 View (眼睛)空间。在视口(viewport)的坐标系中,X 轴指向左侧,Y 轴指向上方,Z 轴指向 View 外(请注意,在右手系统中,Z 轴是 X 轴的叉积轴和 Y 轴)。

  • 投影矩阵:
    投影矩阵描述了从场景的 3D 点到视口(viewport)的 2D 点的映射。投影矩阵从 View 空间变换到裁剪空间,裁剪空间中的坐标被变换为范围为(-1, -1, -1) 到(1, 1, 1) 的归一化设备坐标(NDC)除以剪辑坐标的 w 分量。

如果您想知道几何体中的一个点在视口(viewport)中的哪个位置,则必须进行所有这些转换,并且必须将归一化设备坐标 (NDC) 转换为窗口坐标(像素)。

View 矩阵和投影矩阵的变换由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);

像这样调整函数 getScreenPosition : 函数 getScreenPosition(位置){ var vector = new THREE.Vector3(position.x, position.y, position.z); //模型到世界 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>

关于javascript - three.js Vector3 到 2D 屏幕坐标与旋转场景,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46667395/

相关文章:

javascript - 在历史模式下从子路由刷新时组件被挂载两次

javascript - jquery onclick 函数未定义

javascript - Three.js:导入和使用 3d 模型

math - 3D三角函数: finding a point in space along the surface of a sphere

java - 如何更改 VBO 中的数据?

javascript - 在 jQuery 中调用 Ajax 返回数据的 HTML 元素 ID

javascript - 在 JavaScript 中使用原型(prototype)在现实世界中有什么优势吗?

javascript - Three.js中如何使用多张图片组成一个图形

javascript - 计算单个球体的位置以创建一个由球体组成的球体

javascript - 为什么我的场景在 ThreeJS 中调用 OrbitControl 之前都是黑色的?