Skip to content

Instantly share code, notes, and snippets.

@companje
Last active December 6, 2025 12:47
Show Gist options
  • Select an option

  • Save companje/a6d56672ad6b2eff2c5aa338b668ee4a to your computer and use it in GitHub Desktop.

Select an option

Save companje/a6d56672ad6b2eff2c5aa338b668ee4a to your computer and use it in GitHub Desktop.
Ultimate Sphere rotation code with lat/lon GeoPoints, projections, ortho / dome / lens / cubemap texture, corrections for eye rotation and Weather API
import org.apache.commons.math3.geometry.euclidean.threed.Rotation;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
PGraphics dome, ortho3D, lens, cubemap[] = new PGraphics[6];
PShape globe;
PShader shader;
PImage tex;
float h=512, d=h, hd2=h/2, r=hd2;
int cubemapSize=512;
Rotation qTo = new Rotation(new Vector3D(0, 0, 1), 0);
Rotation qNow = new Rotation(new Vector3D(0, 0, 1), 0);
float progress = .5;
float zoomScaler = .5;
boolean rotationEnabled = true;
boolean locationsVisible = true;
boolean isCamInSphere = false;
boolean isDragging = false;
Weather weather = new Weather();
Heli heli = new Heli();
GeoPoint nl = new GeoPoint(52.37, 4.91);
GeoPoint ny = new GeoPoint(40.79, -73.96);
GeoPoint jp = new GeoPoint(36.14, 137.86);
GeoPoint north = new GeoPoint(90, 0);
GeoPoint south = new GeoPoint(-90, 0);
GeoPoint west = new GeoPoint(0, -90);
GeoPoint east = new GeoPoint(0, 90);
GeoPoint india = new GeoPoint(22, 77);
GeoPoint front = new GeoPoint(0, 0);
GeoPoint places[] = {nl, ny, india, jp, north, south};
GeoPoint selectedLocation = null;
Vector3D eye = new GeoPoint(-30, 0);
void setup() {
size(1536, 512, P3D);
if (h!=height) throw new Error("height error");
tex = createImage(1024, 512, RGB);
dome = createGraphics((int)h, (int)h, P3D);
ortho3D = createGraphics((int)h, (int)h, P3D);
lens = createGraphics((int)h/2, (int)h/2, P3D);
globe = createShape(SPHERE, hd2);
globe.rotateY(HALF_PI);
globe.setStroke(false);
globe.setTexture(loadImage("earth.jpg"));
shader = loadShader("waterworld.glsl");
shader.set("heightmap", loadImage("earth4k_elevation_red_alpha.png"));
shader.set("palette", loadImage("palette-org.png"));
shader.set("borders", loadImage("borders2k.png"));
heli.body = loadImage("heli-body.png");
heli.blades = loadImage("heli-blades.png");
selectLocation(nl);
}
void update() {
qNow = lerp(qNow, qTo, heli.speedFactor);
if (mousePressed || getRotationSpeed()>.1) {
heli.heading = getRotationDirection();
heli.turnSpeed = .8;
} else {
heli.turnSpeed = .08;
}
}
void printWeatherInfo() {
GeoPoint p = new GeoPoint(qTo, front);
weather.request(p);
println("p=", p);
println(weather.name + ", " + weather.country, "gevoelstemperatuur " + weather.feels_like + " graden");
}
void selectLocation(GeoPoint p) {
selectedLocation = p; //bijv new_york
qTo = new Rotation(selectedLocation, front);
rotateToDownUnder(); //update qTo by z-angle to keep south at south
heli.speedFactor = .05;
printWeatherInfo();
}
void draw() {
checkKeys();
update();
background(0);
shader.set("progress", progress);
renderTexture(cubemap, tex);
image(tex, h, 0, 2*h, h);
renderOrtho(ortho3D);
image(ortho3D, 0, 0);
//renderDome(dome);
//image(dome, 0, 0);
drawLens();
//drawTarget();
}
void render(PGraphics pg) {
pg.pushMatrix();
pg.background(0);
pg.shader(shader);
if (rotationEnabled) applyRotation(pg, qNow);
pg.shape(globe);
pg.resetShader();
pg.fill(255, 255, 0);
pg.textAlign(CENTER);
pg.textSize(30);
if (locationsVisible) renderLocations(pg, places);
heli.render(pg);
pg.popMatrix();
}
void renderLocations(PGraphics pg, GeoPoint places[]) {
float o = isCamInSphere ? -1 : 1; //direction for z-movement towards camera
for (GeoPoint l : places) {
pg.pushMatrix();
pg.noStroke();
pg.rotateY(radians(l.lon));
pg.rotateX(radians(l.lat));
pg.translate(0, 0, r + o*5);
pg.stroke(255);
pg.fill(#ff0000);
pg.strokeWeight(1);
pg.ellipse(0, 0, 8, 8);
pg.popMatrix();
}
}
void drawLens() {
renderLens(lens);
pushMatrix();
translate(h+50, 50);
stroke(0);
strokeWeight(1);
noFill();
rect(0, 0, lens.width, lens.height);
image(lens, 0, 0);
popMatrix();
}
void drawTarget() {
noFill();
strokeWeight(2);
stroke(0);
pushMatrix();
translate(h/2, h/2);
ellipse(0, 0, 20, 20);
for (int i=0; i<4; i++) {
line(5, 0, 15, 0);
rotate(HALF_PI);
}
popMatrix();
}
class GeoPoint extends Vector3D {
float lat, lon;
GeoPoint(float lat, float lon) {
super(
cos(radians(lat)) * sin(-radians(lon)),
sin(radians(lat)),
cos(radians(lat)) * cos(-radians(lon)));
this.lat = lat;
this.lon = lon % 180;
}
GeoPoint(Rotation q, GeoPoint p) {
this(-degrees(asin((float) q.applyTo(p).getY())),
-degrees(-atan2((float) q.applyTo(p).getX(), (float) q.applyTo(p).getZ())));
}
String toString() {
return String.format("latlon=(%.1f %.1f) xyz=(%.2f %.2f %.2f)", lat, lon, getX(), getY(), getZ());
}
}
class Heli {
PImage body, blades;
float heading;
float renderScale = 1;
float speedFactor = .1;
float turnSpeed = .1;
void render(PGraphics pg) {
Vector3D axis = qNow.getAxis();
float angle = (float)qNow.getAngle();
pg.pushMatrix();
pg.rotate(angle, (float)axis.getX(), (float)axis.getY(), (float)axis.getZ());
pg.noStroke();
pg.hint(DISABLE_DEPTH_TEST); //needed for heli on texture
pg.stroke(255, 255, 0, 100);
pg.strokeWeight(4);
pg.pushMatrix();
pg.rotate(heading);
pg.translate(0, 0, r + .2);
pg.scale(renderScale);
pg.image(body, -body.width/2, -body.height/2);
pg.popMatrix();
pg.rotate(frameCount*.1);
pg.translate(0, 0, r + .21);
pg.scale(renderScale);
pg.image(blades, -blades.width/2, -blades.height/2);
pg.hint(ENABLE_DEPTH_TEST);
pg.popMatrix();
}
void turnBy(float a) {
heading+=a;
}
Vector3D getNormal() {
return new Vector3D(0, 0, 1); //normal is z-axis.
}
Vector3D getAxis() { //wordt altijd bij aanroep uitgerekend op basis van de huidige z-angle
Vector3D x_axis = new Vector3D(1, 0, 0);
return new Rotation(getNormal(), heading).applyTo(x_axis); //x-axis rotated over z-axis
}
void moveForward(float angle) {
//float angle = .01;
qTo = qTo.compose(new Rotation(getAxis(), angle), RotationConvention.VECTOR_OPERATOR);
}
}
boolean keys[] = new boolean[0xffff+1];
void mouseDragged() {
if (mouseX<h && mouseY<h) {
isDragging = true;
heli.speedFactor = .1;
Vector3D from = getMouseOnSphere(pmouseX, pmouseY, h, h);
Vector3D to = getMouseOnSphere(mouseX, mouseY, h, h);
Rotation r = new Rotation(front, eye);
from = r.applyTo(from); //the camera eye is not in the center so we also
to = r.applyTo(to); //need to rotate the touch input to match the eye
drag(from, to);
}
if (mouseX>h) {
float lat = map(mouseY, 0, h, 90, -90);
float lon = map(mouseX-h, 0, width-h, -180, 180);
GeoPoint p = new GeoPoint(lat, lon);
qTo = getRotationToPoint(p);
}
}
void mouseReleased() {
isDragging = false;
}
void mouseWheel(MouseEvent event) {
if (keyPressed && key=='t') {
progress += event.getCount()*.00001f;
progress = constrain(progress, 0, 1);
}
if (keyPressed && key=='z') {
zoomScaler -= event.getCount()*.001f;
zoomScaler = constrain(zoomScaler, 0, 1);
}
}
void checkKeys() {
float s = 5;
if (keys[RIGHT]) moveLatLonBy(0, s);
if (keys[LEFT]) moveLatLonBy(0, -s);
if (keys[UP]) moveLatLonBy(-s, 0);
if (keys[DOWN]) moveLatLonBy(s, 0);
if (keys['a']) {
heli.turnBy(-heli.turnSpeed);
}
if (keys['d']) {
heli.turnBy(heli.turnSpeed);
}
if (keys['w']) heli.moveForward(.015);
}
void keyPressed() {
keys[key==CODED ? keyCode : key] = true;
if (key>='0' && key<='5') {
selectLocation(places[key-'0']);
}
if (key==' ') {
printWeatherInfo();
}
}
void keyReleased() {
keys[key==CODED ? keyCode : key] = false;
}
void renderLens(PGraphics pg) {
locationsVisible = false;
rotationEnabled = true;
pg.beginDraw();
pg.perspective();
float eyeZ = map(zoomScaler, 0, 1, 310, 400); //280 is echte minimum
pg.camera(0, 0, eyeZ, 0, 0, 0, 0, 1, 0); //340
heli.renderScale = .5;
render(pg);
heli.renderScale = 1;
pg.endDraw();
}
void renderOrtho(PGraphics pg) {
locationsVisible = true;
rotationEnabled = true;
pg.beginDraw();
pg.ortho(-r, r, -r, r, -10, r);
pg.camera((float)eye.getX()*r, (float)eye.getY()*r, (float)eye.getZ()*r, 0, 0, 0, 0, 1, 0);
render(pg);
pg.endDraw();
}
void renderDome(PGraphics pg) {
locationsVisible = true;
rotationEnabled = true;
isCamInSphere = true;
float distToCam = 800; //=extreme //regular: 1900;
pg.beginDraw();
pg.perspective(atan(hd2/distToCam)*2, 1, distToCam, 10000); //fovy, aspect, zNear, zFar
pg.camera(0, 0, -distToCam, 0, 0, 0, 0, 1, 0);
pg.scale(-1, 1, 1);
render(pg);
pg.endDraw();
isCamInSphere = false;
}
void renderTexture(PGraphics pg[], PImage tex) { //via cubemap
locationsVisible = true;
rotationEnabled = false;
isCamInSphere = true; // deze zou aangepast kunnen worden dat de camera buiten de bol zit. of zit ie dat? ivm zichtbaarheid tekst
int c[][] = {
{0, 0, 1, 0, -1, 0},
{0, 0, -1, 0, 1, 0},
{0, -1, 0, 1, 0, 0},
{0, 1, 0, -1, 0, 0},
{1, 0, 0, 0, -1, 0},
{-1, 0, 0, 0, 1, 0}};
for (int i = 0; i < 6; i++) {
if (pg[i]==null) pg[i] = createGraphics(cubemapSize, cubemapSize, P3D);
pg[i].beginDraw();
pg[i].camera(0, 0, 0, c[i][0], c[i][1], c[i][2], c[i][3], c[i][4], c[i][5]);
pg[i].perspective(HALF_PI, 1.0, 1, 1000);
render(pg[i]);
pg[i].endDraw();
pg[i].loadPixels();
}
cubemapToEquirectangular(pg, tex);
isCamInSphere = false;
}
PImage cubemapToEquirectangular(PGraphics pg[], PImage tex) {
tex.loadPixels();
for (int y=0, w=tex.width, h=tex.height; y < h; y++) {
for (int x = 0; x < w; x++) {
float u = map(x, 0, w, -PI, PI);
float v = map(y, 0, h, HALF_PI, -HALF_PI);
PVector dir = new PVector(cos(v) * cos(u), sin(v), cos(v) * sin(u));
tex.pixels[y * w + x] = sampleFromCubemap(pg, dir);
}
}
tex.updatePixels();
return tex;
}
color sampleFromCubemap(PGraphics pg[], PVector dir) {
float ax = abs(dir.x), ay = abs(dir.y), az = abs(dir.z);
boolean xGreatest = ax >= ay && ax >= az, yGreatest = ay >= az;
int face = xGreatest ? (dir.x > 0 ? 0 : 1) :
yGreatest ? (dir.y > 0 ? 2 : 3) :
dir.z > 0 ? 4 : 5;
float uc = xGreatest ? dir.z / ax :
yGreatest ? dir.x / ay :
-dir.x / az;
float vc = (face % 2 == 0 ? 1 : -1) *
(xGreatest ? dir.y / ax :
yGreatest ? dir.z / ay :
dir.y / az);
int px = int(map(uc, -1, 1, 0, pg[face].width - 1));
int py = int(map(vc, -1, 1, 0, pg[face].height - 1));
return pg[face].pixels[py*pg[face].width+px];
}
float getRotationDirection() { //dit wordt gebruikt voor de heading. De hoek tussen de 2 rotaties qTo en qNow
Rotation qDelta = qNow.applyInverseTo(qTo);
Vector3D axis = qDelta.getAxis();
return atan2((float)axis.getY(), (float)axis.getX());
}
float getRotationSpeed() {
Rotation qDelta = qNow.applyInverseTo(qTo);
return (float)qDelta.getAngle();
}
float getZRotationAngle(Rotation rotation) {
double[][] m = rotation.getMatrix(); // Haal de rotatiematrix op
return (float)Math.atan2(m[1][0], m[0][0]); // Bereken de yaw (rotatie rond de Z-as) atan2(sinYaw, cosYaw)
}
void rotateToDownUnder() {
rotateAroundZ(-getZRotationAngle(qTo));
}
void rotateAroundZ(float z_angle) { //relative angle
GeoPoint locationUnderTarget = new GeoPoint(qTo, front);
Rotation z_rot = new Rotation(front, z_angle);
qTo = qTo.applyTo(z_rot);
}
void applyRotation(PGraphics pg, Rotation rotation) {
Vector3D axis = rotation.getAxis();
float angle = (float)rotation.getAngle();
pg.rotate(-angle, (float)axis.getX(), (float)axis.getY(), (float)axis.getZ());
}
Rotation lerp(Rotation start, Rotation end, float t) {
return new Rotation(start.getQ0() + t * (end.getQ0() - start.getQ0()),
start.getQ1() + t * (end.getQ1() - start.getQ1()),
start.getQ2() + t * (end.getQ2() - start.getQ2()),
start.getQ3() + t * (end.getQ3() - start.getQ3()),
true);
}
Vector3D getMouseOnSphere(float x, float y, float w, float h) {
x = map(x, 0, w, -1, 1);
y = map(y, 0, h, -1, 1);
float r = x*x+y*y;
return new Vector3D(x, y, r>1 ? 0 : sqrt(1-r)).normalize();
}
void drag(Vector3D from, Vector3D to) {
Vector3D axis = Vector3D.crossProduct(from, to);
if (axis.getNorm() > 0) {
axis = axis.normalize();
double angle = -acos((float)Vector3D.dotProduct(from, to));
qTo = qTo.compose(new Rotation(axis, angle), RotationConvention.VECTOR_OPERATOR);
}
}
void moveLatLonBy(float latDelta, float lonDelta) {
GeoPoint c = new GeoPoint(qNow, front); // GeoPoint.fromRotation(qNow, front);
GeoPoint p = new GeoPoint(c.lat-latDelta, c.lon+lonDelta);
qTo = new Rotation(p, front);
}
Rotation getRotationToPoint(Vector3D point) {
return new Rotation(point, front);
}
class Weather {
String apiUrl = "https://api.openweathermap.org/data/2.5/weather";
String apikey = "XXX";
float temp_min, feels_like, temp_max, humidity, lat, lon;
String name, country;
PImage icon;
void request(GeoPoint p) {
this.lat = p.lat;
this.lon = p.lon;
String url = apiUrl + "?lat="+lat+"&lon="+lon+"&appid="+apikey+"&units=metric";
JSONObject data = loadJSONObject(url);
JSONObject mainObject = data.getJSONObject("main");
JSONObject weatherObject = data.getJSONArray("weather").getJSONObject(0);
name = data.getString("name");
country = data.getJSONObject("sys").getString("country");
temp_min = mainObject.getFloat("temp_min");
feels_like = mainObject.getFloat("feels_like");
temp_max = mainObject.getFloat("temp_max");
humidity = mainObject.getFloat("humidity");
name = data.getString("name");
country = data.getJSONObject("sys").getString("country");
//icon = loadImage("weather-icons/" + weatherObject.getString("icon") + "[email protected]");
}
}
@companje
Copy link
Author

out4

@companje
Copy link
Author

companje commented Dec 6, 2025

Warning: Processing now sets pixelDensity(2) by default on high-density screens. This may change how your sketch looks. To revert to the old behavior, set pixelDensity(1) in setup().

You can use pixelWidth and pixelHeight instead of width and height.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment