I'm learning 3D rendering with java and I encountered a weird issue.
I'm able to rotate and display 3D objects to the screen. but when I tried to use prospective projection to show depth, the program freaks out. No errors or anything, but the depth seems to stretch incurability long. I narrowed the problem down to the perspective divide in the projection function.
Can anyone help?
public class Renderer extends JPanel{
public Renderer() {
}
double angle = 0;
double a;
double f;
double l;
double offSet;
public void update() {
angle += 0.03;
repaint();
}
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
super.paintComponent(g2d);
for(Triangle element: MidService.mesh)
{
Vertex v1 = rotateY(element.v1);
Vertex v2 = rotateY(element.v2);
Vertex v3 = rotateY(element.v3);
v1 = projection(v1);
v2 = projection(v2);
v3 = projection(v3);
int offSet = (int) (MidService.displayX/2);
g2d.drawLine(offSet + v1.x, offSet + v1.y, offSet + v2.x, offSet + v2.y);
g2d.drawLine(offSet + v2.x, offSet + v2.y, offSet + v3.x, offSet + v3.y);
g2d.drawLine(offSet + v3.x, offSet + v3.y, offSet + v1.x, offSet + v1.y);
}
}
public Vertex projection(Vertex v)
{
a = MidService.displayY / MidService.displayX;
f = 1 / (Math.tan(MidService.fieldOfView/2));
l = MidService.Zfar / (MidService.Zfar - MidService.Znear);
offSet = MidService.Zfar / (MidService.Zfar - MidService.Znear) * MidService.Znear;
double x = (v.x * a * f);
double y = (v.y * f);
double z = (v.z * l - offSet);
double w = v.z;
//the if function below caused the issue
if(w != 0.0) {
x /= w;
y /= w;
z /= w;
}
return new Vertex((int)x, (int)y, (int)z);
}
public Vertex rotateY(Vertex v)
{
double x = v.x * Math.cos(angle) + v.z * Math.sin(angle);
double y = v.y;
double z = v.x * (-Math.sin(angle)) + v.z * Math.cos(angle);
return new Vertex((int)x, (int)y, (int)z);
}
//-------------------------------------------------------
public class MidService {
public static double displayX = 1000;
public static double displayY = 1000;
public static double fieldOfView = 180;
public static double Zfar = 10;
public static double Znear = 1;
public static ArrayList<Triangle> mesh = new ArrayList<>();
}
//---------------------------------------------
public class Driver {
public static void main(String[] args) {
Display display = new Display();
MidService.mesh.add(new Triangle(new Vertex(-100, 100, 100),
new Vertex(-100, -100, 100),
new Vertex(100, -100, 100)));
MidService.mesh.add(new Triangle(new Vertex(-100, 100, 100),
new Vertex(100, -100, 100),
new Vertex(100, 100, 100)));
Here's a video of it:[https://youtu.be/bFJLU5c3JE0]
A follow up to the perspective division issue. It turns out to be the inverse of the difference between camera distance and z coordinate. So instead of division by z, its 1/(distance - z)
Below is modification for Gaba Miau's matrix multiply function
Vertex Mult(float[][] mat,Vertex v){
float ver[] = {v.x,v.y,v.z,v.w};
float ans[] = {0,0,0,0};
for (int i=0;i<mat.length;i++){
for(int j=0;j<mat[0].length;j++){
ans[i] += mat[i][j] * ver[j];
}
}
float temp = 100/(500 - v.z);
if(temp != 0)
{
ans[0] *= temp;
ans[1] *= temp;
ans[2] *= temp;
}
Vertex vans = new Vertex((int)ans[0],(int)ans[1],(int)ans[2]);
vans.w =(int) ans[3];
return vans;
}
P.S. it's ans[i] += mat[i][j] * ver[j]; in the forloop
Firstly you should be using a graphics API like OpenGL or DirectX, secondly you should be using a math library like GLM that contains all the projection matrix formulas and other stuff. Thirdly, you shouldn't be using int as a datatype to store vertex data.
I made a few changes to your code so it doesn't freak out anymore.
The fov should be in the range(0.001,179.999), never 180, it is recommanded using 90 deg. Also the math function tan takes in radians not deg.
package Render;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.sound.midi.MidiDevice;
import javax.swing.JPanel;
import Beans.Triangle;
import Beans.Vertex;
import Utilities.MidService;
public class Renderer extends JPanel{
public Renderer() {
}
double angle = 0;
double a;
double f;
double l;
double offSet;
public void update() {
//angle += 0.03;
repaint();
}
float projMat[][]= {
{1f / ((float)Math.tan(MidService.fieldOfView/2f)),0,0,0},
{0,1f / ((float)Math.tan(MidService.fieldOfView/2f)),0,0},
{0,0,MidService.Zfar / (MidService.Zfar - MidService.Znear),1},
{0,0,-MidService.Zfar* MidService.Znear / (MidService.Zfar - MidService.Znear),0},
};
Vertex Mult(float[][] mat,Vertex v){
float ver[] = {v.x,v.y,v.z,v.w};
float ans[] = {0,0,0,0};
for (int i=0;i<mat.length;i++){
for(int j=0;j<mat[0].length;j++){
ans[i] += mat[i][j] * ver[i];
}
}
Vertex vans = new Vertex((int)ans[0],(int)ans[1],(int)ans[2]);
vans.w =(int) ans[3];
return vans;
}
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
super.paintComponent(g2d);
for(Triangle element: MidService.mesh)
{
Vertex v1 = rotateZ(element.v1);
Vertex v2 = rotateZ(element.v2);
Vertex v3 = rotateZ(element.v3);
v1 = rotateY(v1);
v2 = rotateY(v2);
v3 = rotateY(v3);
v1 = Mult(projMat,v1);
v2 = Mult(projMat,v2);
v3 = Mult(projMat,v3);
int offSet = (int) (MidService.displayX/2);
g2d.drawLine(offSet + (int)v1.x, offSet + (int)v1.y, offSet + (int)v2.x, offSet + (int)v2.y);
g2d.drawLine(offSet + (int)v2.x, offSet + (int)v2.y, offSet + (int)v3.x, offSet + (int)v3.y);
g2d.drawLine(offSet + (int)v3.x, offSet + (int)v3.y, offSet + (int)v1.x, offSet + (int)v1.y);
}
}
public Vertex projection(Vertex v)
{
a = MidService.displayY / MidService.displayX;
f = 1 / (Math.tan(MidService.fieldOfView/2));
l = MidService.Zfar / (MidService.Zfar - MidService.Znear);
offSet = MidService.Zfar / (MidService.Zfar - MidService.Znear) * MidService.Znear;
double x = (v.x * a * f);
double y = (v.y * f);
double z = (v.z * l - offSet);
double w = v.z;
if(w != 0.0) {
x /= w;
y /= w;
z /= w;
}
return new Vertex((int)x, (int)y, (int)z);
}
public Vertex rotateY(Vertex v)
{
double x = v.x * Math.cos(angle) + v.z * Math.sin(angle);
double y = v.y;
double z = v.x * (-Math.sin(angle)) + v.z * Math.cos(angle);
return new Vertex((int)x, (int)y, (int)z);
}
public Vertex rotateZ(Vertex v)
{
double x = v.x * Math.cos(angle) + v.y * (-Math.sin(angle));
double y = v.x * Math.sin(angle) + v.y * Math.cos(angle);
double z = v.z;
return new Vertex((int)x, (int)y, (int)z);
}
}
Here is the MidService class
package Utilities;
import java.util.ArrayList;
import Beans.Triangle;
public class MidService {
public static float displayX = 1000;
public static float displayY = 1000f;
public static float fieldOfView = 3.14f/2f;
public static float Zfar = 100f;
public static float Znear = 0.1f;
public static ArrayList<Triangle> mesh = new ArrayList<>();
}
And Vertex
package Beans;
public class Vertex {
public float x;
public float y;
public float z;
public float w = 1;
public Vertex(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
}
Related
I have a cube that rotates on its 3 axis, when key[a] == true it will spin to the left as if it was rolling that way. rotating the cube 45 degrees in any direction sets it back 90 degrees for the illusion of continuing. this maintains 3 axis that are < 45 degrees off from the environment
I believe this is correct but the x axis for the cube seems to be relative to the environment while y and z are relative to the cubes orientation, I can not find reference to this in the documentation is it a bug?
https://processing.org/reference/rotateY_.html
https://processing.org/reference/rotateX_.html
if(keys[w]) {
if (x >= 359) x = 0;
x = x + 1;
}
if(keys[a]) {
if (z >= 359) z = 0;
z = z + 1;
}
if(keys[s]) {
if (x <= 0) x = 359;
x = x - 1;
}
if(keys[d]) {
if (z <= 0) z = 359;
z = z - 1;
}
// return 90 deg for magic trick
if (x > 45 && x < 180) x = 270 + x;
if (x < 316 && x > 180) x = 360 - x;
if (y > 45 && y < 180) y = 270 + y;
if (y < 316 && y > 180) y = 360 - y;
Matrix transformations are not commutative. The order matters. The matrix operations like rotate() specify a new matrix and multiply the current matrix by the new matrix.
Hence, there is a difference between doing this
rotateX(x);
rotateY(y);
rotateZ(z);
and doing that
rotateZ(z);
rotateY(y);
rotateX(x);
And
rotateX(x1 + x2);
rotateY(y1 + y2);
rotateZ(z1 + z2);
is not the same as
rotateX(x1);
rotateY(y1);
rotateZ(z1);
rotateX(x2);
rotateY(y2);
rotateZ(z2);
One possible solution to your problem would be to use Quaternions. Quaternions behave differently than Euler angles and have also no Gimbal lock issue. Processing use OpenGL under the hood and doesn't support quaternions. However a quaternion can be transformed to a matrix and a matrix can be applied by applyMatrix().
I found this ArcBall example that dose exactly what I wanted. just added a modification to work with keys instead of mouse drag.
ArcBall with mod
// Ariel and V3ga's arcball class with a couple tiny mods by Robert Hodgin & more by me
class Arcball {
float center_x, center_y, radius;
Vec3 v_down, v_drag;
Quat q_now, q_down, q_drag;
Vec3[] axisSet;
int axis;
float mxv, myv;
float x, y;
Arcball(float center_x, float center_y, float radius){
this.center_x = center_x;
this.center_y = center_y;
this.radius = radius;
v_down = new Vec3();
v_drag = new Vec3();
q_now = new Quat();
q_down = new Quat();
q_drag = new Quat();
axisSet = new Vec3[] {new Vec3(1.0f, 0.0f, 0.0f), new Vec3(0.0f, 1.0f, 0.0f), new Vec3(0.0f, 0.0f, 1.0f)};
axis = -1; // no constraints...
}
void rollforward(){
q_down.set(q_now);
v_down = XY_to_sphere(center_x, center_y);
q_down.set(q_now);
q_drag.reset();
v_drag = XY_to_sphere(center_x, center_y - 10);
q_drag.set(Vec3.dot(v_down, v_drag), Vec3.cross(v_down, v_drag));
}
void rolldown(){
q_down.set(q_now);
v_down = XY_to_sphere(center_x, center_y);
q_down.set(q_now);
q_drag.reset();
v_drag = XY_to_sphere(center_x, center_y + 10);
q_drag.set(Vec3.dot(v_down, v_drag), Vec3.cross(v_down, v_drag));
}
void rollleft(){
q_down.set(q_now);
v_down = XY_to_sphere(center_x + radius, center_y + radius);
q_down.set(q_now);
q_drag.reset();
v_drag = XY_to_sphere(center_x + 10 * PI + radius, center_y + radius);
q_drag.set(Vec3.dot(v_down, v_drag), Vec3.cross(v_down, v_drag));
}
void rollright(){
q_down.set(q_now);
v_down = XY_to_sphere(center_x + radius, center_y + radius);
q_down.set(q_now);
q_drag.reset();
v_drag = XY_to_sphere(center_x - 10 * PI + radius, center_y + radius);
q_drag.set(Vec3.dot(v_down, v_drag), Vec3.cross(v_down, v_drag));
}
void mousePressed(){
v_down = XY_to_sphere(mouseX, mouseY); // when m pressed
q_down.set(q_now);
q_drag.reset();
}
void mouseDragged(){
v_drag = XY_to_sphere(mouseX, mouseY);
q_drag.set(Vec3.dot(v_down, v_drag), Vec3.cross(v_down, v_drag));
}
void run(){
q_now = Quat.mul(q_drag, q_down);
applyQuat2Matrix(q_now);
x += mxv;
y += myv;
mxv -= mxv * .01;
myv -= myv * .01;
}
Vec3 XY_to_sphere(float x, float y){
Vec3 v = new Vec3();
v.x = (x - center_x) / radius;
v.y = (y - center_y) / radius;
float mag = v.x * v.x + v.y * v.y;
if (mag > 1.0f){
v.normalize();
} else {
v.z = sqrt(1.0f - mag);
}
return (axis == -1) ? v : constrain_vector(v, axisSet[axis]);
}
Vec3 constrain_vector(Vec3 vector, Vec3 axis){
Vec3 res = new Vec3();
res.sub(vector, Vec3.mul(axis, Vec3.dot(axis, vector)));
res.normalize();
return res;
}
void applyQuat2Matrix(Quat q){
// instead of transforming q into a matrix and applying it...
float[] aa = q.getValue();
rotate(aa[0], aa[1], aa[2], aa[3]);
}
}
static class Vec3{
float x, y, z;
Vec3(){
}
Vec3(float x, float y, float z){
this.x = x;
this.y = y;
this.z = z;
}
void normalize(){
float length = length();
x /= length;
y /= length;
z /= length;
}
float length(){
return (float) Math.sqrt(x * x + y * y + z * z);
}
static Vec3 cross(Vec3 v1, Vec3 v2){
Vec3 res = new Vec3();
res.x = v1.y * v2.z - v1.z * v2.y;
res.y = v1.z * v2.x - v1.x * v2.z;
res.z = v1.x * v2.y - v1.y * v2.x;
return res;
}
static float dot(Vec3 v1, Vec3 v2){
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
static Vec3 mul(Vec3 v, float d){
Vec3 res = new Vec3();
res.x = v.x * d;
res.y = v.y * d;
res.z = v.z * d;
return res;
}
void sub(Vec3 v1, Vec3 v2){
x = v1.x - v2.x;
y = v1.y - v2.y;
z = v1.z - v2.z;
}
}
static class Quat{
float w, x, y, z;
Quat(){
reset();
}
Quat(float w, float x, float y, float z){
this.w = w;
this.x = x;
this.y = y;
this.z = z;
}
void reset(){
w = 1.0f;
x = 0.0f;
y = 0.0f;
z = 0.0f;
}
void set(float w, Vec3 v){
this.w = w;
x = v.x;
y = v.y;
z = v.z;
}
void set(Quat q){
w = q.w;
x = q.x;
y = q.y;
z = q.z;
}
static Quat mul(Quat q1, Quat q2){
Quat res = new Quat();
res.w = q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z;
res.x = q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y;
res.y = q1.w * q2.y + q1.y * q2.w + q1.z * q2.x - q1.x * q2.z;
res.z = q1.w * q2.z + q1.z * q2.w + q1.x * q2.y - q1.y * q2.x;
return res;
}
float[] getValue(){
// transforming this quat into an angle and an axis vector...
float[] res = new float[4];
float sa = (float) Math.sqrt(1.0f - w * w);
if (sa < EPSILON){
sa = 1.0f;
}
res[0] = (float) Math.acos(w) * 2.0f;
res[1] = x / sa;
res[2] = y / sa;
res[3] = z / sa;
return res;
}
}
main
Arcball arcball;
//framecount
int fcount, lastm;
float frate;
int fint = 3;
boolean[] keys = new boolean[4];
final int w = 0;
final int s = 1;
final int a = 2;
final int d = 3;
void setup() {
size(900, 700, P3D);
frameRate(60);
noStroke();
arcball = new Arcball(width/2, height/2+100, 360);
}
void draw() {
lights();
background(255,160,122);
if(keys[w]) { arcball.rollforward(); }
if(keys[a]) { arcball.rollleft(); }
if(keys[s]) { arcball.rolldown(); }
if(keys[d]) { arcball.rollright(); }
ambient(80);
lights();
translate(width/2, height/2-100, 0);
box(50);
translate(0, 200, 0);
arcball.run();
box(50);
}
void keyPressed() {
switch(key) {
case 97:
keys[a] = true;
break;
case 100:
keys[d] = true;
break;
case 115:
keys[s] = true;
break;
case 119:
keys[w] = true;
break;
}
}
void keyReleased() {
switch(key) {
case 97:
keys[a] = false;
break;
case 100:
keys[d] = false;
break;
case 115:
keys[s] = false;
break;
case 119:
keys[w] = false;
break;
}
}
will add support for multiple keys at once later with an edit.... stay tuned
I am attempting to simulate a sphere, and shade it realistically given an origin vector for the light, and the sphere being centered around the origin. Moreover, the light's vector is the normal vector on a larger invisible sphere at a chosen point. The sphere looks off.
https://imgur.com/a/IDIwQQF
The problem, is that it is very difficult to bug fix this kind of program. Especially considering that I know how I want it to look in my head, but when looking at the numbers in my program there is very little meaning attached to them.
Since I don't know where the issue is, I'm forced to paste all of it here.
public class SphereDrawing extends JPanel {
private static final long serialVersionUID = 1L;
private static final int ADJ = 320;
private static final double LIGHT_SPHERE_RADIUS = 5;
private static final double LIGHT_X = 3;
private static final double LIGHT_Y = 4;
private static final double LIGHT_Z = 0;
private static final double DRAWN_SPHERE_RADIUS = 1;
private static final int POINT_COUNT = 1000000;
private static Coord[] points;
private static final double SCALE = 200;
public SphereDrawing() {
setPreferredSize(new Dimension(640, 640));
setBackground(Color.white);
points = new Coord[POINT_COUNT];
initializePoints();
for (int i = 0; i < points.length; i++) {
points[i].scale();
}
new Timer(17, (ActionEvent e) -> {
repaint();
}).start();
}
public void initializePoints() { //finding the points on the surface of the sphere (hopefully somewhat equidistant)
double random = Math.random() * (double)POINT_COUNT;
double offset = 2/(double)POINT_COUNT;
double increment = Math.PI * (3 - Math.sqrt(5));
for (int i = 0; i < POINT_COUNT; i++) {
double y = ((i * offset) - 1) + (offset / 2);
double r = Math.sqrt(1 - Math.pow(y, 2));
double phi = ((i + random) % (double)POINT_COUNT) * increment;
double x = Math.cos(phi) * r;
double z = Math.sin(phi) * r;
points[i] = new Coord(x, y, z);
}
}
public void drawSphere(Graphics2D g) {
g.translate(ADJ, ADJ); //shifting from origin for drawing purposes
Arrays.sort(points); //sorting points by their z coordinates
double iHat = -2 * LIGHT_X;
double jHat = -2 * LIGHT_Y; //Light vector
double kHat = -2 * LIGHT_Z;
double angL1 = 0;
if (Math.abs(iHat) != 0.0)
angL1 = Math.atan(jHat / iHat); //converting light vector to spherical coordinates
else
angL1 = Math.PI/2;
double angL2 = Math.atan(Math.sqrt(Math.pow(iHat, 2) + Math.pow(jHat, 2))/ kHat);
double maxArcLength = LIGHT_SPHERE_RADIUS * Math.PI; // maximum arc length
for (int i = 0; i < points.length; i++) {
if(points[i].checkValid()) {
double siHat = -2 * points[i].x;
double sjHat = -2 * points[i].y; //finding normal vector for the given point on the sphere
double skHat = -2 * points[i].z;
double angSF1 = -1 * Math.abs(Math.atan(sjHat / siHat)); // converting vector to spherical coordinates
double angSF2 = Math.atan(Math.sqrt(Math.pow(siHat, 2) + Math.pow(sjHat, 2))/ skHat);
double actArcLength = LIGHT_SPHERE_RADIUS * Math.acos(Math.cos(angL1) * Math.cos(angSF1) + Math.sin(angL1) * Math.sin(angSF1) * Math.cos(angL2 - angSF2)); //calculating arc length at this point
double comp = actArcLength / maxArcLength; // comparing the maximum arc length to the calculated arc length for this vector
int col = (int)(comp * 255);
col = Math.abs(col);
g.setColor(new Color(col, col, col));
double ovalDim = (4 * Math.PI * Math.pow(DRAWN_SPHERE_RADIUS, 2))/POINT_COUNT; //using surface area to determine how large size of each point should be drawn
if (ovalDim < 1) // if it too small, make less small
ovalDim = 2;
g.fillOval((int)points[i].x, (int)points[i].y, (int)ovalDim, (int)ovalDim); //draw this oval
}
}
}
#Override
public void paintComponent(Graphics gg) {
super.paintComponent(gg);
Graphics2D g = (Graphics2D) gg;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
drawSphere(g);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setTitle("Sphere");
f.setResizable(false);
f.add(new SphereDrawing(), BorderLayout.CENTER);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
});
}
#SuppressWarnings("rawtypes")
private class Coord implements Comparable {
public double x;
public double y;
public double z;
public Coord(double x2, double y2, double z2) {
x = x2;
y = y2;
z = z2;
}
public void scale() {
x *= SCALE;
y *= SCALE; //drawing purposes
z *= SCALE;
}
public String toString() {
return x + " " + y + " " + z;
}
public int compareTo(Object c) {
double diff = this.z - ((Coord)c).z;
if (diff < 0)
return -1;
else if (diff > 0) //for sorting the array of points
return 1;
else
return 0;
}
public boolean checkValid() {
return (z > 0); //checks if need to draw this point
}
}
}
I was hoping to at least draw a realistic looking sphere, even if not completely accurate, and I couldn't tell you what exactly is off with mine
Currently I have an ArrayList of vertices in a 3-dimensional cartesian coordinates system. The polygon is random. It can be a car, a cup or even a dragon.
Assuming the density does not change, how to calculate the centre of mass (x,y,z) of this 3D object?
I am storing the faces and vertices in ArrayList.
public ArrayList<stlFace> StlFaces = new ArrayList<stlFace>();
public ArrayList<VertexGeometric> VertexList = new ArrayList<VertexGeometric>();
I was using this for calculating surface which is proportional to mass of each face or triangle. And to calculate center off mass of each triangle and center of mass of whole object I was using this. I added helper methods getCenter() and getSurface() to Face class to encapsulate calculations specific to just one face/triangle.
public static class Vertex {
public float x = 0;
public float y = 0;
public float z = 0;
public Vertex(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
}
}
public static class Face {
public Vertex v1;
public Vertex v2;
public Vertex v3;
public Face(Vertex v1, Vertex v2, Vertex v3) {
this.v1 = v1;
this.v2 = v2;
this.v3 = v3;
}
public Vertex getCenter() {
Vertex triangleCenter = new Vertex(0, 0, 0);
triangleCenter.x += v1.x;
triangleCenter.x += v2.x;
triangleCenter.x += v3.x;
triangleCenter.y += v1.y;
triangleCenter.y += v2.y;
triangleCenter.y += v3.y;
triangleCenter.z += v1.z;
triangleCenter.z += v2.z;
triangleCenter.z += v3.z;
triangleCenter.x /= 3;
triangleCenter.y /= 3;
triangleCenter.z /= 3;
return triangleCenter;
}
public float getSurface() {
float x1 = v1.x - v2.x;
float x2 = v1.y - v2.y;
float x3 = v1.z - v2.z;
float y1 = v1.x - v3.x;
float y2 = v1.y - v3.y;
float y3 = v1.z - v3.z;
return (float) Math.sqrt(
Math.pow(x2 * y3 - x3 * y2, 2) +
Math.pow(x3 * y1 - x1 * y3, 2) +
Math.pow(x1 * y2 - x2 * y1, 2)
) / 2f;
}
}
public static Vertex calculateMassCenter(List<Face> faces) {
Vertex massCenter = new Vertex(0, 0, 0);
float mass = 0;
for (Face face : faces) {
Vertex triangleCenter = face.getCenter();
float faceMass = face.getSurface();
mass += faceMass;
massCenter.x += faceMass * triangleCenter.x;
massCenter.y += faceMass * triangleCenter.y;
massCenter.z += faceMass * triangleCenter.z;
}
massCenter.x /= mass;
massCenter.y /= mass;
massCenter.z /= mass;
return massCenter;
}
I'm getting unexpected results when multiplying two quaternions and applying the resulting rotation to my local right-handed coordinate system. (X pointing forward, Y to the right and Z downward).
(See my Java SCCE below)
So I am trying to first apply a Z rotation by 90 degrees (yaw) and then a rotation of 90 degrees around the local X axis (roll).
I am trying to accomplish this by multiplying two quaternions representing these two rotations, creating a rotation Matrix from the result and applying it to the 3 unit vectors of my coordinate system but the results I am getting do not make sense. (i.e. they do not represent the coordinate system you should get from these two rotations.)
I have tried changing the quaternion multiplication order which did not help (see code lines that were commented out in the main method of the SCCE).
I have also tried creating the quaternion for the second rotation from global Y to simulate that it was created from the resulting local coordinate system after the first rotation.
For reference I am also calculating the result by applying the two individual rotation matrices (which works as expected).
What am I doing wrong?
import java.text.DecimalFormat;
import java.text.NumberFormat;
public class Quaternion {
public static final double NORMALIZATION_LOWER_TOLERANCE = 1 - 1e-4;
public static final double NORMALIZATION_UPPER_TOLERANCE = 1 + 1e-4;
private double w = 1.0;
private double x = 0.0;
private double y = 0.0;
private double z = 0.0;
public static void main(String[] args) {
Vector3D xVect = new Vector3D(1,0,0);
Vector3D yVect = new Vector3D(0,1,0);
Vector3D zVect = new Vector3D(0,0,1);
System.out.println("Initial Local Coordinate System: X:"+xVect+" / Y:"+yVect+ " / Z:"+zVect);
Quaternion rotZ = new Quaternion(Math.PI/2, zVect); // Yaw +90 deg
Quaternion rotY = new Quaternion(Math.PI/2, yVect); // Yaw +90 deg
Quaternion rotX = new Quaternion(Math.PI/2, xVect); // Then roll +90 deg
Matrix rotationMatrixZ = new Matrix(rotZ);
Vector3D localX = xVect.rotate(rotationMatrixZ);
Vector3D localY = yVect.rotate(rotationMatrixZ);
Vector3D localZ = zVect.rotate(rotationMatrixZ);
System.out.println("New Local Coordinate System after Yaw: X:"+localX+" / Y:"+localY+ " / Z:"+localZ); // Gives expected result
Quaternion localRotX = new Quaternion(Math.PI/2, localX);
Matrix localRotXMatrix = new Matrix(localRotX);
Vector3D rotatedX = localX.rotate(localRotXMatrix);
Vector3D rotatedY = localY.rotate(localRotXMatrix);
Vector3D rotatedZ = localZ.rotate(localRotXMatrix);
System.out.println("New Local Coordinate System two local rotations: X:"+rotatedX+" / Y:"+rotatedY+ " / Z:"+rotatedZ); // Gives expected result
Quaternion rotZX = rotZ.multiply(rotX);
// Quaternion rotZX = rotX.multiply(rotZ); // Tried both orders
// Quaternion rotZX = rotZ.multiply(rotY); // rotY is in fact the local rotX
// Quaternion rotZX = rotZ.multiply(rotY); // rotY is in fact the local rotX, tried both orders
rotZX.normalizeIfNeeded();
Matrix rotationXMatrixZX = new Matrix(rotZX);
rotatedX = xVect.rotate(rotationXMatrixZX);
rotatedY = localY.rotate(rotationXMatrixZX);
rotatedZ = localZ.rotate(rotationXMatrixZX);
System.out.println("New Local Coordinate System Quaternion Multiplication: X:"+rotatedX+" / Y:"+rotatedY+ " / Z:"+rotatedZ); // Expect same as above
}
public Quaternion() {
}
public Quaternion(double w, double x, double y, double z) {
this.w = w;
this.x = x;
this.y = y;
this.z = z;
}
public Quaternion(double angle, Vector3D vector){
double halfAngle = angle / 2;
double sin = Math.sin(halfAngle);
this.w = Math.cos(halfAngle);
this.x = vector.getX()*sin;
this.y = vector.getY()*sin;
this.z = vector.getZ()*sin;
}
public boolean normalizeIfNeeded() {
double sum = w * w + x * x + y * y + z * z;
if (NORMALIZATION_LOWER_TOLERANCE < sum && sum < NORMALIZATION_UPPER_TOLERANCE) {
return false;
}
double magnitude = Math.sqrt(sum);
w /= magnitude;
x /= magnitude;
y /= magnitude;
z /= magnitude;
return true;
}
public Quaternion multiply(Quaternion q2) {
Quaternion result = new Quaternion();
result.w = w * q2.w - x * q2.x - y * q2.y - z * q2.z;
result.x = w * q2.x + x * q2.w + y * q2.z - z * q2.y;
result.y = w * q2.y - x * q2.z + y * q2.w + z * q2.x;
result.z = w * q2.z + x * q2.y - y * q2.x + z * q2.w;
return result;
}
public Quaternion conjugate() {
return new Quaternion(w, -x, -y, -z);
}
public double getW() {
return w;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getZ() {
return z;
}
#Override
public String toString() {
return "Quaternion [w=" + w + ", x=" + x + ", y=" + y + ", z=" + z + "]";
}
static class Vector3D {
double x=0;
double y=0;
double z=0;
public Vector3D(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public Vector3D rotate(Matrix rotationMatrix){
return rotationMatrix.multiply(this);
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getZ() {
return z;
}
#Override
public String toString() {
NumberFormat df = DecimalFormat.getNumberInstance();
return "[x=" + df.format(x) + ", y=" + df.format(y) + ", z=" + df.format(z) + "]";
}
}
static class Matrix {
private double[][] values;
public Matrix(int rowCount, int colCount) {
values = new double[rowCount][colCount];
}
public Matrix(Quaternion quaternionForRotationMatrix) {
this(3,3);
double w = quaternionForRotationMatrix.getW();
double x = quaternionForRotationMatrix.getX();
double y = quaternionForRotationMatrix.getY();
double z = quaternionForRotationMatrix.getZ();
double ww = w*w;
double wx = w*x;
double xx = x*x;
double xy = x*y;
double xz = x*z;
double wy = w*y;
double yy = y*y;
double yz = y*z;
double wz = w*z;
double zz = z*z;
values[0][0] = ww + xx - yy - zz;
values[0][1] = 2 * xy - 2 * wz;
values[0][2] = 2 * xz + 2 * wy;
values[1][0] = 2 * xy + 2 * wz;
values[1][1] = ww - xx + yy - zz;
values[1][2] = 2 * yz + 2 * wx;
values[2][0] = 2 * xz - 2 * wy;
values[2][1] = 2 * yz - 2 * wx;
values[2][2] = ww - xx - yy + zz;
}
public Vector3D multiply(Vector3D vector){
double [][] vect = new double [3][1];
vect[0][0] = vector.getX();
vect[1][0] = vector.getY();
vect[2][0] = vector.getZ();
double [][] result = multiplyMatrices(values, vect);
return new Vector3D(result[0][0], result[1][0], result[2][0]);
}
private double[][] multiplyMatrices(double[][] m1, double[][] m2) {
double[][] result = null;
if (m1[0].length == m2.length) {
int rowCount1 = m1.length;
int colCount1 = m1[0].length;
int rowCount2 = m2[0].length;
result = new double[rowCount1][rowCount2];
for (int i = 0; i < rowCount1; i++) {
for (int j = 0; j < rowCount2; j++) {
result[i][j] = 0;
for (int k = 0; k < colCount1; k++) {
result[i][j] += m1[i][k] * m2[k][j];
}
}
}
} else {
int rowCount = m1.length;
int colCount = m1[0].length;
result = new double[rowCount][colCount];
for (int i = 0; i < m1.length; i++) {
for (int j = 0; j < m1[0].length; j++) {
result[i][j] = 0;
}
}
}
return result;
}
#Override
public String toString() {
StringBuffer sb = new StringBuffer("Matrix = ");
for(int row = 0 ; row<values.length; row++){
sb.append ("[ ");
for(int col = 0 ; col<values[0].length; col++){
sb.append(Double.toString(values[row][col]));
if(col<values.length-1){
sb.append(" | ");
}
}
sb.append("] ");
}
return sb.toString();
}
}
}
Nevermind. Found it. I had an error in the formulas to build the rotation matrix. It now works as expected.
I am making a mental note to use formulas from Wikipedia in the future and not some random other site.
The respective part should be
values[0][0] = ww + xx - yy - zz;
values[0][1] = 2 * xy - 2 * wz;
values[0][2] = 2 * xz + 2 * wy;
values[1][0] = 2 * xy + 2 * wz;
values[1][1] = ww - xx + yy - zz;
values[1][2] = 2 * yz - 2 * wx; //CORRECTED SIGN
values[2][0] = 2 * xz - 2 * wy;
values[2][1] = 2 * yz + 2 * wx; //CORRECTED SIGN
values[2][2] = ww - xx - yy + zz;
At the end of the main method I was also using the wrong vectors for y and z:
Matrix rotationXMatrixZX = new Matrix(rotZX);
rotatedX = xVect.rotate(rotationXMatrixZX);
rotatedY = yVect.rotate(rotationXMatrixZX); // Corrected used y-vector
rotatedZ = zVect.rotate(rotationXMatrixZX); // Corrected used z-vector
I have made some progress detecting a specific kind of object. Actually a card, just like any other in your wallet.
Now I'm stuck with deskewing the photo. See:
The blue (rounded) rectangle represents the detected contour.
The purple rotate rectangle represents a RotatedRect extracted from the detected contour.
The green line is just the bounding box.
Well I need neither of those rectangles. The rectangles both have 90 degree corners. Which won't get me the perspective.
My question:
How can I get as accurate as possible all quadrangle corners from a contour?
I have created a class Quadrangle which creates the quadrangle of the 4 most largest connected polygon vertices which will intersect each other at some point. This will work in nearly any case.
If you use this code, remember to adjust the width and height in Quadrangle.warp. Note that it isn't 100% complete, the first and last polygon vertices won't be connected if they may be connect for example.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
class Line {
public Point offset;
public double angle;
public Line(Point offset, double angle) {
this.offset = offset.clone();
this.angle = angle;
}
public Point get(int length) {
Point result = offset.clone();
result.x += Math.cos(angle) * length;
result.y += Math.sin(angle) * length;
return result;
}
public Point getStart() {
return get(-5000);
}
public Point getEnd() {
return get(5000);
}
public void scale(double factor) {
offset.x *= factor;
offset.y *= factor;
}
public static Point intersect(Line l1, Line l2) {
return getLineLineIntersection(l1.getStart().x, l1.getStart().y, l1.getEnd().x, l1.getEnd().y,
l2.getStart().x, l2.getStart().y, l2.getEnd().x, l2.getEnd().y
);
}
public static Point getLineLineIntersection(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) {
double det1And2 = det(x1, y1, x2, y2);
double det3And4 = det(x3, y3, x4, y4);
double x1LessX2 = x1 - x2;
double y1LessY2 = y1 - y2;
double x3LessX4 = x3 - x4;
double y3LessY4 = y3 - y4;
double det1Less2And3Less4 = det(x1LessX2, y1LessY2, x3LessX4, y3LessY4);
if (det1Less2And3Less4 == 0){
// the denominator is zero so the lines are parallel and there's either no solution (or multiple solutions if the lines overlap) so return null.
return null;
}
double x = (det(det1And2, x1LessX2,
det3And4, x3LessX4) /
det1Less2And3Less4);
double y = (det(det1And2, y1LessY2,
det3And4, y3LessY4) /
det1Less2And3Less4);
return new Point(x, y);
}
protected static double det(double a, double b, double c, double d) {
return a * d - b * c;
}
}
class LineSegment extends Line implements Comparable {
public double length;
public LineSegment(Point offset, double angle, double length) {
super(offset, angle);
this.length = length;
}
public void melt(LineSegment segment) {
Point point = new Point();
point.x += Math.cos(angle) * length;
point.y += Math.sin(angle) * length;
point.x += Math.cos(segment.angle) * segment.length;
point.y += Math.sin(segment.angle) * segment.length;
angle = Math.atan2(point.y, point.x);
offset.x = (offset.x * length + segment.offset.x * segment.length) / (length + segment.length);
offset.y = (offset.y * length + segment.offset.y * segment.length) / (length + segment.length);
length += segment.length;
}
#Override
public int compareTo(Object other) throws ClassCastException {
if (!(other instanceof LineSegment)) {
throw new ClassCastException("A LineSegment object expected.");
}
return (int) (((LineSegment) other).length - this.length);
}
}
class Quadrangle {
static int
TOP = 0,
RIGHT = 1,
BOTTOM = 2,
LEFT = 3;
public Line[] lines = new Line[4];
public Quadrangle() {
}
private static double getAngle(Point p1, Point p2) {
return Math.atan2(p2.y - p1.y, p2.x - p1.x);
}
private static double getLength(Point p1, Point p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
private static double roundAngle(double angle) {
return angle - (2*Math.PI) * Math.round(angle / (2 * Math.PI));
}
public static Quadrangle fromContour(MatOfPoint contour) {
List<Point> points = contour.toList();
List<LineSegment> segments = new ArrayList<>();
// Create line segments
for (int i = 0; i < points.size(); i++) {
double a = getAngle(points.get(i), points.get((i + 1) % points.size()));
double l = getLength(points.get(i), points.get((i + 1) % points.size()));
segments.add(new LineSegment(points.get(i), a, l));
}
// Connect line segments
double angleDiffMax = 2 * Math.PI / 100;
List<LineSegment> output = new ArrayList<>();
for (LineSegment segment : segments) {
if (output.isEmpty()) {
output.add(segment);
} else {
LineSegment top = output.get(output.size() - 1);
double d = roundAngle(segment.angle - top.angle);
if (Math.abs(d) < angleDiffMax) {
top.melt(segment);
} else {
output.add(segment);
}
}
}
Collections.sort(output);
Quadrangle quad = new Quadrangle();
for (int o = 0; o < 4; o += 1) {
for (int i = 0; i < 4; i++) {
if (Math.abs(roundAngle(output.get(i).angle - (2 * Math.PI * o / 4))) < Math.PI / 4) {
quad.lines[o] = output.get(i);
}
}
}
return quad;
}
public void scale(double factor) {
for (int i = 0; i < 4; i++) {
lines[i].scale(factor);
}
}
public Mat warp(Mat src) {
Mat result = src.clone();
Core.line(result, lines[TOP].get(-5000), lines[TOP].get(5000), new Scalar(200, 100, 100), 8);
Core.line(result, lines[RIGHT].get(-5000), lines[RIGHT].get(5000), new Scalar(0, 255, 0), 8);
Core.line(result, lines[BOTTOM].get(-5000), lines[BOTTOM].get(5000), new Scalar(255, 0, 0), 8);
Core.line(result, lines[LEFT].get(-5000), lines[LEFT].get(5000), new Scalar(0, 0, 255), 8);
Point p = Line.intersect(lines[TOP], lines[LEFT]);
System.out.println(p);
if (p != null) {
Core.circle(result, p, 30, new Scalar(0, 0, 255), 8);
}
double width = 1400;
double height = width / 2.15;
Point[] srcProjection = new Point[4], dstProjection = new Point[4];
srcProjection[0] = Line.intersect(lines[TOP], lines[LEFT]);
srcProjection[1] = Line.intersect(lines[TOP], lines[RIGHT]);
srcProjection[2] = Line.intersect(lines[BOTTOM], lines[LEFT]);
srcProjection[3] = Line.intersect(lines[BOTTOM], lines[RIGHT]);
dstProjection[0] = new Point(0, 0);
dstProjection[1] = new Point(width - 1, 0);
dstProjection[2] = new Point(0, height - 1);
dstProjection[3] = new Point(width - 1, height - 1);
Mat warp = Imgproc.getPerspectiveTransform(new MatOfPoint2f(srcProjection), new MatOfPoint2f(dstProjection));
Mat rotated = new Mat();
Size size = new Size(width, height);
Imgproc.warpPerspective(src, rotated, warp, size, Imgproc.INTER_LINEAR);
return rotated;
}
}