Okay, it took some noodling, but I came up with a theoretical solution that might sort-of-kind-of work for your situation. Here are the basic principles:
- Around the center of an equirectangular projection the amount of distortion is minimal (the horizontal degrees per pixel remains similar).
- Mapping equirectangular pixel coordinates to polar coordinates is trivial.
- If we convert polar coordinates into a 3d vector we can use axis angle rotation to translate a set of vectors from one area of the sphere to another.
- We can convert from 3d vectors to polar coordinates, and thus back into 2d equirectangular texture coordinates.
This approach would require each drawing instruction to be constrained to a limited size in degrees, and positions would need to be on polar coordinates, but in theory you could take any set of 2d drawing instructions and make the paint onto a sphere with limited distortion. The big downside of this approach is that it is really slow, especially for high resolution textures.
// How many degrees are covered by the drawing area (if you draw something bigger then this won't work)
const drawingDegrees = 30;
const drawingSize = 256;
const drawingDegreesPerPixel = drawingDegrees / drawingSize;
const texGraphicsSize = 512;
const texGraphicsDegreesPerPixel = 180 / texGraphicsSize;
let glContext;
let texGraphics;
let drawing;
let latSlider;
let longSlider;
let radiusSlider;
let strokeSlider;
let bg;
let latAxis;
let longAxis;
let center;
function setup() {
let c = createCanvas(windowWidth, windowHeight, WEBGL);
glContext = c.GL;
angleMode(DEGREES);
noStroke();
texGraphics = createGraphics(texGraphicsSize * 2, texGraphicsSize);
drawing = createGraphics(drawingSize, drawingSize);
drawing.ellipseMode(RADIUS);
drawing.fill('red');
drawing.stroke('blue');
latSlider = createSlider(0, 180, 90);
longSlider = createSlider(0, 360, 180);
// circle radius in degrees
radiusSlider = createSlider(1, 15, 5);
// circle stroke in pixels
strokeSlider = createSlider(1, 20, 2);
latSlider.position(10, 10);
longSlider.position(10, 30);
radiusSlider.position(10, 50);
strokeSlider.position(10, 70);
latSlider.changed(updateGraphics);
longSlider.changed(updateGraphics);
radiusSlider.changed(updateGraphics);
strokeSlider.changed(updateGraphics);
bg = color(200);
center = createVector(0, 0, 1);
latAxis = createVector(1, 0, 0);
longAxis = createVector(0, 1, 0);
updateGraphics();
}
function updateGraphics() {
texGraphics.background(bg);
texGraphics.stroke(0);
texGraphics.strokeWeight(4);
texGraphics.line(0, texGraphicsSize / 2, texGraphicsSize * 2, texGraphicsSize / 2);
texGraphics.line(texGraphicsSize, 0, texGraphicsSize, texGraphicsSize);
texGraphics.loadPixels();
drawing.clear();
drawing.push();
drawing.translate(drawingSize / 2, drawingSize / 2);
drawing.strokeWeight(strokeSlider.value());
drawing.circle(0, 0, radiusSlider.value() / drawingDegreesPerPixel);
drawing.pop();
drawing.loadPixels();
let drawingCenter = rotateAround(
rotateAround(center, latAxis, latSlider.value() - 90),
longAxis,
longSlider.value() - 180
);
// In order to map each pixel of our texture to a pixel in the drawing we will
// need to find the 3d vector for the point on a sphere and transform it by
// this axis-angle rotation
let transformAxis = p5.Vector.cross(drawingCenter, center);
let transformAngle = drawingCenter.angleBetween(center);
// For each pixel in the texture
for (let y = 0; y < texGraphicsSize; y++) {
for (let x = 0; x < texGraphicsSize * 2; x++) {
// Calculate the polar coordinates for this pixel in the texture
let texLat = (y - (texGraphicsSize / 2)) * texGraphicsDegreesPerPixel;
let texLong = (x - texGraphicsSize) * texGraphicsDegreesPerPixel;
let texVector = rotateAround(
rotateAround(center, latAxis, texLat),
longAxis,
texLong
);
let drawingVector = rotateAround(
texVector,
transformAxis,
transformAngle
);
// Find the polar cordinates of the transformed vector.
// Project the vector onto the plane throught the equator.
let planarVector =
p5.Vector.sub(
drawingVector,
p5.Vector.mult(longAxis, drawingVector.dot(longAxis))
);
// The angle between the planarVector and the drawingVector is the latitude
// (-90 to 90)
let drawingLat =
planarVector.angleBetween(drawingVector) *
Math.sign(drawingVector.dot(longAxis));
// The angle between the planarVector and the reference direction is the
// longitude (-180 to 180)
let drawingLong =
planarVector.angleBetween(center) *
Math.sign(planarVector.dot(latAxis));
// Convert the lat long into a pixel position
let drawingX = drawingLong / drawingDegreesPerPixel + drawingSize / 2;
let drawingY = drawingLat / drawingDegreesPerPixel + drawingSize / 2;
// It would be nice if there were a more efficient way to bound the outer
// loop to avoid iterations where this check is false.
if (drawingX >= 0 && drawingX < drawingSize &&
drawingY >= 0 && drawingY < drawingSize) {
// Sample the pixel value
let c = drawing.get(drawingX, drawingY);
let cNoAlpha = color(red(c), green(c), blue(c));
texGraphics.set(x, y, lerpColor(color(texGraphics.get(x, y)), cNoAlpha, alpha(c) / 255));
}
// Otherwise this texture position is outside of the drawing area
}
}
texGraphics.updatePixels();
}
function draw() {
background(255);
texture(texGraphics);
push();
camera(0, 0, (height/2.0) / tan(30.0), 0, 0, 0, 0, 1, 0);
ortho();
translate(-width / 2, -height / 2);
rect(0, 0, 400, 200);
pop();
glContext.clear(glContext.DEPTH_BUFFER_BIT);
orbitControl(2, 2, 0.1);
ambientLight(100, 100, 100);
directionalLight(155, 155, 155, 0.5, 0.5, -1);
rotateY(180);
sphere(200, 40, 20);
}
// Rotate one vector (vect) around another (axis) by the specified angle.
function rotateAround(vect, axis, angle) {
// Make sure our axis is a unit vector
axis = p5.Vector.normalize(axis);
return p5.Vector.add(
p5.Vector.mult(vect, cos(angle)),
p5.Vector.add(
p5.Vector.mult(
p5.Vector.cross(axis, vect),
sin(angle)
),
p5.Vector.mult(
p5.Vector.mult(
axis,
p5.Vector.dot(axis, vect)
),
(1 - cos(angle))
)
)
);
}
Runnable example: Sphere Mapped 2d Drawing - OpenProcessing