Creating a three-dimensional representation by projecting onto the surface of a sphere in ThreeJS

3D animation is a realm filled with numerous terms and concepts that are unfamiliar to me. I am particularly confused about the significance of "UV" in 3D rendering and the tools used for mapping pixels onto a mesh.

I have an image captured by a 360-degree camera, which I intend to use as the texture for a sphere. However, I want the center of this image to represent the top of the sphere, with the circular radius translating into an arc along the surface from top to bottom.

Starting with code snippets borrowed directly from Three.JS documentation:

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();

The output result exhibits some issues:

  • The texture orientation needs correction (rotated 90 degrees)
  • The ground seems distorted, potentially linked to the rotation issue?
  • Update: Further examination reveals that the image aligns properly with the sphere when considering its top and bottom poles; however, the edges create a distorted effect resembling sideways ground textures
  • The current setup places the image on the outside of the sphere, but my goal is to project it onto the inside while positioning the camera within the sphere

Placing the camera inside results in a solid black display. As per Three.JS guidelines, lighting should not be required with a MeshBasicMaterial. The likely problem could stem from outward-facing normals on the sphere's faces, suggesting a need for reversal akin to skybox implementations that I'm unsure how to execute.

Research indicates that adjusting the "UV"s may solve this issue, although the method and implication remain unclear...

Answer №1

Example in Action

I took inspiration from @manthrax's CodeSandbox.io project and made some modifications of my own:

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

The Breakthrough

After dedicating a full day to unraveling the concept of UV mapping for better comprehension, I delved into applying trigonometry to map points from a sphere onto my stereographic image. The process boiled down to the following steps:

  1. Using arccosine of the Y coordinate to ascertain the magnitude of a polar coordinate on the stereographic image
  2. Employing the arctangent of the X and Z coordinates to determine the angle of the polar coordinate on the stereographic image
  3. Utilizing x = Rcos(theta), y = Rsin(theta) to calculate the rectangular coordinates on the stereographic image

If time allows, I might create a visual aid using Illustrator or similar software to illustrate the mathematical principles, though it essentially boils down to standard trigonometry.

Further exploration led me to address an issue stemming from the 240-degree vertical viewing angle of my camera, causing distortions in the image especially near ground level. By subtracting the vertical viewing angle from 360 and dividing by two, a specific angle relative to the vertical axis emerged, marking a threshold where no mapping should transpire. Aligning the sphere along the Y-axis facilitated correlating this angle with a particular Y-coordinate - delineating areas with data above and below accordingly.

  1. Determine the "minimum Y value"
  2. For all sphere points:
    • If a point surpasses the minimum Y value, linearly scale it so that the initial occurrence is considered as "0," while the top still signifies "1" for mapping purposes
    • If a point falls beneath the minimum Y value, exclude it from the mapping

Unusual Observations

Curiously, my code implementation resulted in an upside-down image rendering. The root cause could be attributed to inaccuracies in my trigonometric calculations or potential misconceptions regarding UV maps. Regardless, rectifying this anomaly was straightforward - simply rotating the sphere by 180 degrees post-mapping.

In addressing uncertainty around how to "exclude" values in the UV map, I opted to assign all points below the minimum Y value to the image corner (displayed as black).

With a 240-degree viewing angle, the vacant space at the bottom of the sphere devoid of image data appeared notably prominent on my screen. Displeased with this visual artifact, I substituted 270 for the vertical FOV, albeit resulting in minor distortions near ground level compared to when employing 360.

The Implementation

Presented here is the code segment responsible for updating the UV mappings:

// Insert 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;
            }
        }
    }
    // Correcting the inverted UV mapping by adjusting the math and rotating the sphere by 180 degrees
    geometry.rotateZ(Math.PI);
    geometry.uvsNeedUpdate = true;
}
updateUVs();

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

The Outcome

Integrating this mesh into a scene yields flawless results:

https://i.sstatic.net/MKwlN.png https://i.sstatic.net/UqwGj.png https://i.sstatic.net/JnnYz.png https://i.sstatic.net/aLNj1.png

An Enigma Yet Unsolved

Notably, around the "hole" at the sphere's base, an intriguing multi-colored ring emerges resembling a mirrored reflection of the sky. The presence of this phenomenon remains puzzling to me, raising questions about its origin and significance. Any insights or clarifications shared in the comments would be greatly appreciated!

Answer №2

After spending about 10 minutes experimenting with a polar unwrapping of the uv's, here is the closest result I achieved.

If you want to improve the mapping, feel free to tweak the polarUnwrap function...

Check out the code here

https://i.sstatic.net/2QJqr.jpg

You can swap out TextureLoader().loadTexture() with

//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;

to incorporate your video into the design...

For more details, visit:

Three.js VideoTexture documentation

You might also find this resource helpful:

Guide on displaying dual fisheye videos with Three.js

Answer №3

Modifying the UVs to make the stereographic projected image fit can be quite challenging. The UVs of a sphere are specifically designed to align with textures using equirectangular projection.

If you need to convert the image from stereographic to equirectangular, tools like PTGui or Hugin could be helpful. Alternatively, Photoshop can be used by applying Filter > Distort > Polar Coordinates > polar to rectangular.

https://i.sstatic.net/aJqct.jpg An example of equirectangular projection created with Photoshop, resized to a 2:1 aspect ratio (not required for texture mapping)

To ensure the texture is inside the sphere (or normals flipped), set the material to THREE.BackSide.

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

You may also need to horizontally flip the texture, as discussed in How to flip a Three.js texture horizontally.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Selenium Tips: Ensuring RemoteDriver Remains Connected to the Active Browser Tab

Currently working on a unique Windows application that utilizes voice commands to control web browsers. I am trying to figure out the best approach when users add tabs and modify the selected tab as needed. Unfortunately, RemoteDriver only supports swi ...

Ionic: setInterval/setTimer not functioning after 5 minutes in the background

In need of a timer that can send notifications via the OneSignal API after a user-defined time period is reached. Users can set the timer for any value between 1-59 minutes. Despite attempts to use the background mode plugin, specifically setInterval and s ...

Ensure the md-sidenav remains open by default

I have set up a md-sidenav in my project. I would like the sidenav to be open by default, while still maintaining an overlay effect on the page and allowing users to close the sidenav, thus avoiding the use of md-is-locked-open. Any suggestions on how I ca ...

Automate populating input fields with data

I need help with a form that has four input boxes. Is it possible to automatically fill the remaining three input boxes with the value entered in the first box when the user clicks a button using JavaScript? Or should I aim to prefill the textboxes with ...

Utilizing HTML documents instead of images in Owl Carousel 2

I am currently utilizing owl carousel 2 to construct a basic sliding carousel. However, I am only using images at the moment and would like to incorporate HTML files instead. These HTML files contain multiple divs where images can be inserted, and instead ...

What is the best way to create a self-referencing <div> in HTML?

After extensive research, I turn to seeking advice and guidance on Stack Exchange. I have a seemingly straightforward goal in mind. I want to create a <div> id/class that will automatically generate a link to itself using scripting of some sort. Be ...

Tips for organizing and sorting date data in a JSON datatable

I am working with two date input fields: <form id="refresh" method="post" action="income.php"> <input type="text" id="dari" name="dari" /> <input type="text" id="sampai" name="sampai" /> <button type="submit">Refresh</b ...

AWS Lambda applies quotes to create the event input

I'm encountering a problem with my Python 3.8 AWS Lambda function that is meant to handle form inputs from a web application. The data from the form inputs gets passed to the Lambda function and ends up in the event dictionary. However, lambda seems t ...

JavaScript Regular Expression Assistance Domain Snatcher

I am in the process of developing a JavaScript Regex and I am struggling to find the right pattern to match a specific domain. Let me provide an example for better understanding. I need a regex that can identify any domain that exclusively contains (text. ...

Incorporating a new data series into your candlestick chart with Highstock

I've encountered an issue with adding a data series to my candlestick chart in Highstock using Highcharts. Here's the script I'm using: $.ajax({ url : 'indicator', type : 'GET', data ...

Error with Laravel and Vue template integration

I have recently started using Vue in Laravel 5.3 and I am able to retrieve data through a jQuery AJAX request within Vue. However, I am facing difficulties with displaying the data. Below is my current script in the view: <li> <!-- inner men ...

Is it possible to use the .focus() event on an entire form in JQuery? If so, how

Take a look at this HTML snippet: <form ....> <textarea>....</textarea <input .... /> </form> I'm trying to set up a help section that appears when the user focuses on any form element, and disappears when they lose ...

JSON function invocation is failing to load

This is my JavaScript script, When I call the function calculate(), the JSON method ConvertDataTabletoString() only gets called once, when I load the page. I want the method to be called every 5 seconds. I am calling a VB.NET function in .aspx. How can ...

An error is triggered by serializing a TinyBox POST form into an Array

When I implemented the code below, it performed as anticipated: TINY.box.show({url:target, post:$("form[name='currentSearch']").serialize(), width:650, mask:true, close:true, maskid:'boxMask', boxid:'popupBox', openjs:funct ...

"Successfully uploading files with Ajax on Postman, however encountering issues when attempting to do so in

I am currently working on allowing specific users to upload videos to the API using Ajax. The process works smoothly in Postman, however, when attempting the same action from a web browser, I encounter a 500 Internal Server Error. Unfortunately, I do not ...

Address Book on Rails

Hello, I'm relatively new to this and would be grateful for any assistance. My goal is to utilize the data saved by a user in their address book, and then offer them the option to use that address for delivery. Below is my Address controller: class A ...

Working with npm objects across multiple files

My goal is to integrate the npm package for parallax (lax.js) into my project. The documentation states that in order to initialize it, the following code snippet should be included: window.onload = function () { lax.init() // Add a driver that we use ...

Ways to showcase angular scope data within a placeholder while avoiding the use of angular expressions

Initially, I utilized angular expressions {{value}} to present values within elements. However, upon noticing that unrevealed expressions continue to display on the front end during loading delays, I switched to using ng-bind. <div> <h1>Hell ...

Unable to access res.name due to subscription in Angular

Right now, I am following a tutorial on YouTube that covers authentication with Angular. However, I have hit a roadblock: The code provided in the tutorial is not working for me because of the use of subscribe(), which is resulting in the following messag ...

Sending forms through the .NET platform

On my .NET page, I have implemented client-side validation for a submit button within the form element. var register = $('#<%= Register.ClientId %>'); register.click(function(){ validation.init(); return false; }); Disabling the f ...