SarahC
SarahC

Reputation: 113

Finding length of arc on unit circle only given x position

Some background: I've been trying to map a texture onto a "sphere" using a look up table of texture co-ordinate changes. This is for a really slow micro controller to draw on a little LCD panel. So Three.JS is out, WebGL etc... the look up table should work!

The equations for texturing a sphere all pinch the poles. I can't "pre-spread" the texture of these extremes because the texture offset changes to make the "sphere" appear to rotate.

If you examine the code for making the lookup table here, you'll see the approach, and the running demo shows the issue.

https://codepen.io/SarahC/pen/KKoKqKW

I figured I'd try and come up with a new approach myself! After thinking a while, I realised a sphere texture in effect moves the location of the texture pixel further from the spheres origin the further away from the origin it is! In a straight line from the origin.

So I figured - calculate the angle the current pixel is from the origin, calculate it's unit distance, then all I need to do is make a function that is given the distance, and calculates the new distance based on some "sphere calculation". That new distance is almost 1 to 1 near the center of the sphere, and rapidly increases right at the edges. Mapping a sphere texture!

That offset function I figured (may be wrong here?) (diagrammed below) given the distance from the origin L1 (unit circle) it returns the length of the arc L2 which in effect is the flat pixel offset to use from the source texture.

(I asked on Reddit, and got given Math.acos for X, but now I know that's wrong, because that's the X position of the circle! Not the straight line X position from the offset, AND it gives an angle, not the Y position... wrong on two counts. Oooph! Oddly, surprisingly, because I thought it gave the Y position, I dropped it into an atan2 function, and it ALMOST worked... for all the wrong reasons of course but it made a bump at the center of the "sphere". The current "state of the mistake" is right here: https://codepen.io/SarahC/pen/abYbgwa?editors=0010 )

Now I know that aCos isn't the function I need, I'm at a loss for the solution.

But! Perhaps this approach I thought up is stupid? I'd happily use any look-up mapping function you think would work. =)

Thanks for your time and reading and sharing, I like learning new stuff.

Diagram

//JS

Upvotes: 1

Views: 173

Answers (1)

Trentium
Trentium

Reputation: 3729

An interesting but befuddling problem...

Per Spektre's comment and my follow up comment, the mapping of x to the length of the circle's arc still resulted in the center bubble effect of the image as described in the question. I tried numerous mathematically "correct" attempts, including picking a distant view point from the sphere and then calculating the compression of the 2d image as the view point angle swept from top center of the sphere to the edge, but again, to no avail, as the bubble effect persisted...

In the end, I introduced a double fudge factor. To eliminate the bubble effect, I took the 32nd root of the unit radius to stretch the sphere's central points. Then, when calculating the arc length (per the diagram in the question and the comments on "L2") I undid the stretching fudge factor by raising to the 128th power the unit radius to compress and accentuate the curvature towards the edge of the sphere.

Although this solution appears to provide the proper results, it offends the mathematician in me, as it is a pure fudge to eliminate the confusing bubble effect. The use of the 32nd root and 128th power were simply arrived at via trial and error, rather than any true mathematical reasoning. Ugh...

So, FWIW, the code snippet below exemplifies both the calculation and use of the lookup table in functions unitCircleLut2() and drawSphere2(), respectively...

// https://www.reddit.com/r/GraphicsProgramming/comments/vlnqjc/oldskool_textured_sphere_using_lut_and_texture_xy/

// Perhaps useable as a terminator eye?........
// https://www.youtube.com/watch?v=nSlEQumWLHE
// https://www.youtube.com/watch?v=hx_0Ge4hDpI

// This is an attempt to recreate the 3D eyeball that the Terminator upgrade produces on the Adafruit M4sk system.
// As the micro control unit only has 200Kb RAM stack and C and no 3D graphics support, chances are there's no textured 3D sphere, but a look up table to map an eyeball texture to a sphere shape on the display.

// I've got close - but this thing pinches the two poles - which I can't see happening with the M4sk version.



// Setup the display, and get its pixels so we can write to them later.
let c = document.createElement("canvas");
c.width = 300;
c.height = 300;
document.body.appendChild(c);
let ctx = c.getContext("2d");
let imageDataWrapper = ctx.getImageData(0, 0, c.width, c.height);
let imageData = imageDataWrapper.data; // 8 bit ARGB
let imageData32 = new Uint32Array(imageData.buffer); // 32 bit pixel


// Declare the look up table - dimensions same as display.
let offsetLUT = null;


// Texture to map to sphere.
let textureCanvas = null;
let textureCtx = null;
let textureDataWrapper = null;
let textureData = null;
let textureData32 = null;

let px = 0;
let py = 0;
let vx = 2;
let vy = 0.5;

// Load the texture and get its pixels.
let textureImage = new Image();
textureImage.crossOrigin = "anonymous";
textureImage.onload = _ => {  
  textureCanvas = document.createElement("canvas");
  textureCtx = textureCanvas.getContext("2d");

  offsetLUT = unitCircleLut2( 300 );

  textureCanvas.width = textureImage.width;
  textureCanvas.height = textureImage.height;
  textureCtx.drawImage(textureImage, 0, 0);
  textureDataWrapper = textureCtx.getImageData(0, 0, textureCanvas.width, textureCanvas.height);
  textureData = textureDataWrapper.data;
  textureData32 = new Uint32Array(textureData.buffer);
  // Draw texture to display - just to show we got it.
  // Won't appear if everything works, as it will be replaced with the sphere draw.
  for(let i = 0; i < imageData32.length; i++) {
    imageData32[i] = textureData32[i];
  }
  ctx.putImageData(imageDataWrapper, 0, 0);
  
  requestAnimationFrame(animation); 
}

textureImage.src = "https://untamed.zone/miscImages/metalEye.jpg";


function unitCircleLut2( resolution ) {
  
  function y( x ) {
    // x ** 128 compresses when x approaches 1.  This helps accentuate the
    // curvature of the sphere near the edges...
    return ( Math.PI / 2 - Math.acos( x ** 128 ) ) / ( Math.PI / 2 );
  }
  
  let r = resolution / 2 |0;
  
  // Rough calculate the length of the arc...
  let arc = new Array( r );
  for ( let i = 0; i < r; i++ ) {
    // The calculation for nx stretches x when approaching 0.  This removes the
    // center bubble effect...
    let nx = ( i / r ) ** ( 1 / 32 );
    arc[ i ] = { x: nx, y: y( nx ), arcLen: 0 };
    if ( 0 < i ) {
      arc[ i ].arcLen = arc[ i - 1 ].arcLen + Math.sqrt( ( arc[ i ].x - arc[ i - 1 ].x ) ** 2 + ( arc[ i ].y - arc[ i - 1 ].y ) ** 2 );
    }
  }
  
  let arcLength = arc[ r - 1 ].arcLen;

  // Now, for each element in the array, calculate the factor to apply to the
  // metal eye to either stretch (center) or compress (edges) the image...
  
  let lut = new Array( resolution );
  let centerX = r;
  let centerY = r;
  
  for( let y = 0; y < resolution; y++ ) {
    let ny = y - centerY;
    lut[ y ] = new Array( resolution );
    
    for( let x = 0; x < resolution; x++ ) {
      let nx = x - centerX;
      let nd = Math.sqrt( nx * nx + ny * ny ) |0;

      if ( r <= nd ) {
        lut[ y ][ x ] = null;
      } else {
        lut[ y ][ x ] = arc[ nd ].arcLen / arcLength;
      }
    }
  }
  
  return lut;
  
}


function drawSphere2(dx, dy){
  const mx = textureCanvas.width - c.width;
  const my = textureCanvas.height - c.height;
  const idx = Math.round(dx);
  const idy = Math.round(dy);
  
  const textureCenterX = textureCanvas.width / 2 |0;
  const textureCenterY = textureCanvas.height / 2 |0;

  let iD32index = 0;
  for(let y = 0; y < c.height; y++){
    for(let x = 0; x < c.width; x++){
      let stretch = offsetLUT[y][x];
      if(stretch == null){ 
        imageData32[iD32index++] = 0;
      }else{
        // The 600/150 is the ratio of the metal eye to the offsetLut.  But, since the
        // eye doesn't fill the entire image, the ratio is fudged to get more of the
        // eye into the sphere...
        let tx = ( x - 150 ) * 600/150 * Math.abs( stretch ) + textureCenterX + dx |0;
        let ty = ( y - 150 ) * 600/150 * Math.abs( stretch ) + textureCenterY + dy |0;
        let textureIndex = tx + ty * textureCanvas.width;
        imageData32[iD32index++] = textureData32[textureIndex];
      }
    }
  }
  ctx.putImageData(imageDataWrapper, 0, 0);
}


// Move the texture on the sphere and keep redrawing.
function animation(){
  px += vx;
  py += vy;  
  let xx = Math.cos(px / 180 * Math.PI) * 180 + 0;
  let yy = Math.cos(py / 180 * Math.PI) * 180 + 0;
  drawSphere2(xx, yy);
  requestAnimationFrame(animation);
}
body {
  background: #202020;
  color: #f0f0f0;
  font-family: arial;
}

canvas {
  border: 1px solid #303030;
}

Upvotes: 0

Related Questions