Last active
February 22, 2025 21:17
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]"); | |
} | |
} |
Author
companje
commented
Feb 22, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment