Skip to content

Instantly share code, notes, and snippets.

@companje
Last active February 22, 2025 21:17
Show Gist options
  • Save companje/a6d56672ad6b2eff2c5aa338b668ee4a to your computer and use it in GitHub Desktop.
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

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