在 ThreeJS 中将立体投影映射到球体内部

Posted

技术标签:

【中文标题】在 ThreeJS 中将立体投影映射到球体内部【英文标题】:Mapping a stereographic projection to the inside of a sphere in ThreeJS 【发布时间】:2018-09-28 18:04:21 【问题描述】:

谈到 3D 动画,我不熟悉 很多 的术语和概念(可能是附加到这个问题的第二个问题:有什么好书可以熟悉这些概念?)。我不知道什么是“UV”(在 3D 渲染的上下文中),而且我不熟悉将图像上的像素映射到网格上的点的工具。

以下图像是由 360 度相机生成的(它实际上是 html video 元素的输出):

我希望这张图片的中心是球体的“顶部”,并且这张图片中圆的任何半径都是沿着球体从上到下的弧线。

这是我的起点(直接从 Three.JS 文档复制代码行):

var video = document.getElementById( "texture-video" );

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

var texture = new THREE.VideoTexture( video );
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.format = THREE.RGBFormat;

var material = new THREE.MeshBasicMaterial(  map: texture  );

var geometry = new THREE.SphereGeometry(0.5, 100, 100);
var mesh = new THREE.Mesh( geometry, material );

scene.add( mesh );

camera.position.z = 1

function animate()

    mesh.rotation.y += 0.01;
    requestAnimationFrame( animate );
    renderer.render( scene, camera );

animate();

这会产生以下内容:

有几个问题:

纹理旋转90度 地面扭曲,虽然如果旋转固定,这可能是固定的? 更新:在对正在生成的球体进行进一步调查后,它实际上并没有旋转 90 度。相反,图像的顶部是球体的顶部,图像的底部是球体的底部。这导致图像的左右边缘变成了我看到的扭曲的“侧身地面” 这是在球体的外部。我想将其投影到球体的内部(并将相机放置在球体内部)

目前,如果我将相机放在球体内,我会得到纯黑色。我认为这不是照明问题,因为 Three.JS 文档说 MeshBasicMaterial 不需要照明。我认为问题可能是所有球体面的法线都指向外面,我需要反转它们。我不确定如何做到这一点 - 但我很确定这是可能的,因为我认为天空盒就是这样工作的。

做一些研究我很确定我需要修改“UV”来解决这个问题,我只是不知道这甚至意味着什么或真正意味着什么......

【问题讨论】:

【参考方案1】:

工作示例

我分叉了@manthrax 的 CodeSandbox.io 解决方案,并用我自己的解决方案进行了更新:

https://codesandbox.io/s/4w1njkrv9

解决方案

因此,在花了一天时间研究 UV 映射以了解它的含义及其工作原理之后,我能够坐下来划出一些三角函数来将球体上的点映射到我的立体图像上的点。它基本上归结为以下几点:

    使用 Y 坐标的反余弦来确定立体图像上极坐标的大小 使用 X 和 Z 坐标的反正切来确定立体图像上极坐标的角度 使用x = Rcos(theta), y = Rsin(theta) 计算立体图像上的直角坐标

如果时间允许,我可能会在 Illustrator 中快速绘制图像或其他东西来解释数学,但这是标准三角函数

在这之后我更进一步,因为我使用的相机只有 240 度的垂直视角 - 这导致图像略微失真(尤其是在地面附近)。通过从 360 中减去垂直视角并除以 2,您可以从垂直方向获得一个角度,在该角度范围内不应发生映射。由于球体沿 Y 轴定向,因此该角度映射到特定的 Y 坐标 - 上方有数据,下方没有数据。

    计算这个“最小 Y 值” 对于球体上的所有点: 如果该点高于最小 Y 值,则对其进行线性缩放,以便将第一个此类值计为“0”,而球体的顶部仍计为“1”以用于映射目的 如果该点低于最小 Y 值,则不返回任何内容

奇怪的警告

由于某种原因,我编写的代码将图像颠倒过来。我不知道我是搞砸了我的三角函数还是搞砸了我对 UV 贴图的理解。无论如何,在映射后将球体翻转 180 度即可轻松解决此问题

同样,我不知道如何在 UV 贴图中“不返回任何内容”,因此我将所有低于最小 Y 值的点映射到图像的角落(黑色)

在 240 度视角下,球体底部没有图像数据的空间足够大(在我的显示器上),我可以在直视前方时看到黑色圆圈。我不喜欢它的视觉外观,所以我为垂直 FOV 插入了 270。这会导致地面周围的轻微变形,但没有使用 360 时那么严重。

代码

这是我为更新 UV 贴图而编写的代码:

// Enter the vertical FOV for the camera here
var vFov = 270; // = 240;

var material = new THREE.MeshBasicMaterial(  map: texture, side: THREE.BackSide  );
var geometry = new THREE.SphereGeometry(0.5, 200, 200);

function updateUVs()

    var maxY = Math.cos(Math.PI * (360 - vFov) / 180 / 2);
    var faceVertexUvs = geometry.faceVertexUvs[0];
    // The sphere consists of many FACES
    for ( var i = 0; i < faceVertexUvs.length; i++ )
    
        // For each face...
        var uvs = faceVertexUvs[i];
        var face = geometry.faces[i];
        // A face is a triangle (three vertices)
        for ( var j = 0; j < 3; j ++ )
        
            // For each vertex...
            // x, y, and z refer to the point on the sphere in 3d space where this vertex resides
            var x = face.vertexNormals[j].x;
            var y = face.vertexNormals[j].y;
            var z = face.vertexNormals[j].z;

            // Because our stereograph goes from 0 to 1 but our vertical field of view cuts off our Y early
            var scaledY = (((y + 1) / (maxY + 1)) * 2) - 1;

            // uvs[j].x, uvs[j].y refer to a point on the 2d texture
            if (y < maxY)
            
                var radius = Math.acos(1 - ((scaledY / 2) + 0.5)) / Math.PI;
                var angle = Math.atan2(x, z);

                uvs[j].x = (radius * Math.cos(angle)) + 0.5;
                uvs[j].y = (radius * Math.sin(angle)) + 0.5;
             else 
                uvs[j].x = 0;
                uvs[j].y = 0;
            
        
    
    // For whatever reason my UV mapping turned everything upside down
    // Rather than fix my math, I just replaced "minY" with "maxY" and
    // rotated the sphere 180 degrees
    geometry.rotateZ(Math.PI);
    geometry.uvsNeedUpdate = true;

updateUVs();

var mesh = new THREE.Mesh( geometry, material );

结果

现在,如果您将此网格添加到场景中,一切看起来都很完美:

我仍然不明白的一件事

在球体底部的“孔”周围有一个多色环。它几乎看起来像一面天空的镜子。我不知道它为什么存在或它是如何到达那里的。任何人都可以在 cmets 中阐明这一点吗?

【讨论】:

该环可能是因为低于该阈值的 UV 在该行三角形中回绕到 0。不错的映射!看起来比我一起破解的要干净得多。 干得好! manthrax 是对的,关于戒指是对的。根据您在立体图像中的哪个季度,低于阈值的 UV 应该映射到最近的角落:0,00,11,01,1 codesandbox 示例没有(更多)工作。【参考方案2】:

这是我在 10 分钟左右摆弄 uv 的极性展开后所能得到的最接近的结果。

您可以修改 polarUnwrap 函数来尝试获得更好的映射......

https://codesandbox.io/s/8nx75lkn28

您可以将 TextureLoader().loadTexture() 替换为

//assuming you have created a HTML video element with id="video"
var video = document.getElementById( 'video' );

var texture = new THREE.VideoTexture( video );
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.format = THREE.RGBFormat;

让您的视频在那里播放...

更多信息在这里:

https://threejs.org/docs/#api/textures/VideoTexture

这也可能对你有用:

https://community.theta360.guide/t/displaying-thetas-dual-fisheye-video-with-three-js/1160

【讨论】:

我找到了一个改进的解决方案。认为您可能有兴趣看到它。【参考方案3】:

我认为,修改 UV 以使立体投影图像适合是相当困难的。球体的 UV 设置为适合具有 equirectangular 投影的纹理。

要将图像从立体图像转换为等距矩形图像,您可能需要使用全景工具,例如 PTGui 或 Hugin。或者您可以使用 Photoshop(应用滤镜 > 扭曲 > 极坐标 > 极坐标到矩形)。

图像的等距矩形投影(使用 Photoshop),调整为 2:1 纵横比(纹理不需要)

如果您希望纹理位于球体内部(或法线翻转),您可以将材质设置为THREE.BackSide

var material = new THREE.MeshBasicMaterial(  map: texture, side: THREE.BackSide  );

也许,你必须水平翻转纹理然后:How to flip a Three.js texture horizontally

【讨论】:

不幸的是,这对我不起作用,因为立体输出来自实时提要。 也许您可以使用 D3.js 库的地理投影来实时转换图像。这里有一个相关的例子:Stereographic Blue Marble 我找到了解决问题的方法。认为您可能会感兴趣。

以上是关于在 ThreeJS 中将立体投影映射到球体内部的主要内容,如果未能解决你的问题,请参考以下文章

THREEJS在360立体视频球体前显示一个object3D

360视频:正八面体投影OHP

基于threejs的酷炫年会抽奖

C++ openGl 纹理将图像映射到球体上

将云投影指向2D

js + html5 怎么来绘制三维图形