3D Textured rolling ball

Hi !

I’m trying to do a (seemingly) easy thing but I am having a lot of trouble doing it.
I want to simulate a sphere rolling on a plane, with a texture on it.
To do so I computed the vector around which the sphere should rotate at each frame : it is the cross product of the velocity vector and the vector pointing upward.
After that I created the 4*4 Matrix representing the 3D rotation of the sphere around that axis (of a small angle).
But now I have trouble applying this matrix to the sphere.
Any help would be greatly appreciated (I’m quite new to processing !)
Thanks :wink:

EDIT : I posted the code below

Here is the code I have so far :

float depth = 1000; // Camera parameter
float boxWidth = 1000; 
float boxThick = 16;
float radius = 20; //Radius of the sphere
float vx = 0;
float vz = 0;
PImage img;
PShape globe;

PVector y0 = new PVector(0,1,0);

Mover mover;

void settings() {
  size(1500,1500, P3D);
  mover = new Mover();
}

void setup() {
  noStroke();
  img = loadImage("tpj.jpg");
  globe = createShape(SPHERE, radius);
  globe.setStroke(false);
  globe.setTexture(img);


}

void draw() {
  camera(0, -0.5*depth, depth, 0, 0, 0, 0, 1, 0);
  directionalLight(50, 100, 125, 0, 1, 0);
  ambientLight(102, 102, 102);
  background(200);
  pushMatrix();
  box(boxWidth,boxThick,boxWidth);
  popMatrix();
  lights();
  
  mover.update();
  mover.checkEdges();
  mover.display();

  
}

void keyPressed() {
  if (key == CODED) {
    if (keyCode == UP) {
      vx += -0.1;
    }
    else if (keyCode == DOWN) {
      vx += 0.1;
    }
    else if (keyCode == RIGHT) {
      vz += 0.1;
    }
    else if (keyCode == LEFT) {
      vz += -0.1;
    }
  }
}


The Mover class :

float reboundConstant = 1;
float angle = 0;

PMatrix result = null;

class Mover {

  PVector location;
  PVector velocity;
  PVector force;

Mover() {
  location = new PVector(0, 0, 0);
  velocity = new PVector(0, 0, 0);
}

void update() {
  PVector v = new PVector(vz,0,vx);
  velocity.add(v);
  velocity.y = 0;
  location.add(velocity); 

}

void display() {
  noStroke();
  translate(location.x, -(radius + 0.5*boxThick), location.z);
  rotating();
  shape(globe);
}

void rotating() {
  result = getMatrix();
  angle = velocity.mag()/radius;
  PVector cross = y0.cross(velocity);
  cross.y = 0;
  cross.normalize();
  float ux = cross.x;
  float uy = 0;
  float uz = -cross.z;
  float c = cos(angle);
  float s = sin(angle);
  
  float r11 = ux*ux*(1-c)+c;
  float r12 = ux*uy*(1-c)-uz*s;
  float r13 = ux*uz*(1-c)+uy*s;
  float r21 = ux*uy*(1-c)+uz*s;
  float r22 = uy*uy*(1-c)+c;
  float r23 = uy*uz*(1-c)-ux*s;
  float r31 = ux*uz*(1-c)-uy*s;
  float r32 = uy*uz*(1-c)+ux*s;
  float r33 = uz*uz*(1-c)+c; 

  result.apply(r11,r12,r13,0,r21,r22,r23,0,r31,r32,r33,0,0,0,0,1);
  setMatrix(result);
  applyMatrix(result);
  pushMatrix();
  shape(globe);
  popMatrix();

  
  
}  

void checkEdges() {
  if (location.x > 0.5*boxWidth - radius) {
    location.x = 0.5*boxWidth - radius;
    velocity.x *= -reboundConstant;
  }
  else if (location.x < -0.5*boxWidth + radius) {
    location.x = -0.5*boxWidth + radius;
    velocity.x *= -reboundConstant;
  }
  if (location.z > 0.5*boxWidth - radius) {
    location.z = 0.5*boxWidth - radius;
    velocity.z *= -reboundConstant;
  }
  else if (location.z < -0.5*boxWidth + radius) {
    location.z = -0.5*boxWidth + radius;
    velocity.z *= -reboundConstant;
  }
}
}

The rotating() bit is not working at all…

1 Like

Hi @Thomzoy,

Some general advice for 3D:

  • Use PMatrix3D, a class, instead of PMatrix, an interface.
  • Processing’s unconventional world coordinate system makes even the simplest orientation problem a mess. Experiment with camera and perspective or ortho until you get a more conventional world coordinate system. Two conventions are Z+ as world up axis or Y+ as world up.
  • Draw a transform for the world coordinates when debugging.
  • Create an MCVE. (I.e., whether or not the ball is textured makes no difference to the rotation problem.)

Your rotating function looks like a mix of two functions usually found in a affine transform matrix: rotateAngleAxis and lookAt. PMatrix3D has an overload for rotate which supports an arbitrary axis. A lookAt function takes a forward direction (or two points representing an origin and destination) and a reference to the world up direction, then constructs a matrix from the object’s local forward, right and up axes. So far as I know, it’s not in PMatrix3D, but various tutorials to implement it are available throughout the web.

For the first example, I just used rotateZ and rotateY.

static final float SQRT_3_2 = sqrt(3.0) / 2.0;
PShape sphere;
int sphereDetail = 12;
float sphereSize = 150.0;
PMatrix3D sphereMat = new PMatrix3D();
float theta = 0.0;
float phi = 0.0;
PVector dir = new PVector();
float speed = 0.125;

void setup() {
  size(512, 256, P3D);
  ellipseMode(RADIUS);
  smooth(8);
  sphereDetail(sphereDetail);

  // Create sphere.
  sphere = createShape(SPHERE, sphereSize);
  sphere.setFill(false);
  sphere.setStrokeWeight(0.5);
  sphere.setStroke(0xff202020);
}

void draw() {
  float camDist = height * SQRT_3_2;
  theta = frameCount * 0.01;
  phi = TAU * mouseX / (float)(width - 1);
  PVector.fromAngle(phi, dir);
  dir.mult(frameCount * speed);

  // Update sphere matrix.
  sphereMat.reset();
  sphereMat.translate(dir.x, dir.y, dir.z);
  sphereMat.rotateZ(phi);
  sphereMat.rotateY(theta);

  // Apply sphere matrix to shape.
  sphere.resetMatrix();
  sphere.applyMatrix(sphereMat);

  surface.setTitle(String.format("%.0f", 
    degrees(phi)));
  background(0xffffffff);

  // Draw camera with z-up world coordinates.
  perspective(THIRD_PI, -width / (float)height, 
    0.01, 1000.0);
  camera(camDist, -camDist, camDist, 
    0.0, 0.0, 0.0, 
    0.0, 0.0, -1.0);

  drawArc(0.0, 0.0, 150.0, phi, 1.5);
  drawTransform(125.0, 1.25);
  shape(sphere);
}

void drawArc(float cx, float cy, float r, float t, float sw) {
  noFill();
  strokeWeight(sw);
  stroke(0xffff7f00);
  arc(cx, cy, r, r, 
    0.0, t, PIE);
}

void drawTransform(float len, float sw) {
  strokeWeight(sw);
  stroke(0xffff0000);
  line(0.0, 0.0, 0.0, len, 0.0, 0.0);

  stroke(0xff00ff00);
  line(0.0, 0.0, 0.0, 0.0, len, 0.0);

  stroke(0xff0000ff);
  line(0.0, 0.0, 0.0, 0.0, 0.0, len);
}

A variant of the above with the lookAt function and orthographic projection:

static final float SQRT_3_2 = sqrt(3.0) / 2.0;
PShape sphere;
int sphereDetail = 12;
float sphereSize = 150.0;
PMatrix3D sphereMat = new PMatrix3D();
float theta = 0.0;
float phi = 0.0;
PVector dir = new PVector();
float speed = 0.125;

PVector worldUp = new PVector(0.0, 0.0, 1.0);
PVector xLocal = new PVector();
PVector yLocal = new PVector();
PVector zLocal = new PVector();

void setup() {
  size(512, 256, P3D);
  ellipseMode(RADIUS);
  smooth(8);
  sphereDetail(sphereDetail);

  sphere = createShape(SPHERE, sphereSize);
  sphere.setFill(false);
  sphere.setStrokeWeight(0.5);
  sphere.setStroke(0xff202020);
}

void draw() {
  float camDist = height * SQRT_3_2;
  float halfw = width * 0.5;
  float halfh = height * 0.5;
  theta = frameCount * 0.01;
  phi = TAU * mouseX / (float)(width - 1);
  PVector.fromAngle(phi, dir);
  dir.mult(frameCount * speed);

  sphereMat.reset();
  lookAt(dir, PVector.add(dir, dir),
    worldUp, sphereMat,
    xLocal, yLocal, zLocal);
  sphereMat.rotate(-theta, 1.0, 0.0, 0.0);

  sphere.resetMatrix();
  sphere.applyMatrix(sphereMat);

  surface.setTitle(String.format("%.0f", 
    degrees(phi)));
  background(0xffffffff);

  ortho(halfw, -halfw, -halfh, halfh);
  camera(camDist, -camDist, camDist, 
    0.0, 0.0, 0.0, 
    0.0, 0.0, -1.0);

  drawArc(0.0, 0.0, 150.0, phi, 1.5);
  drawWorldTransform(125.0, 1.25);
  drawLocalTransform(75.0, 1.25, dir, xLocal, yLocal, zLocal);
  shape(sphere);
}

void drawArc(float cx, float cy, float r, float t, float sw) {
  noFill();
  strokeWeight(sw);
  stroke(0xffff7f00);
  arc(cx, cy, r, r, 
    0.0, t, PIE);
}

void drawWorldTransform(float len, float sw) {
  strokeWeight(sw);
  stroke(0xffff0000);
  line(0.0, 0.0, 0.0, len, 0.0, 0.0);

  stroke(0xff00ff00);
  line(0.0, 0.0, 0.0, 0.0, len, 0.0);

  stroke(0xff0000ff);
  line(0.0, 0.0, 0.0, 0.0, 0.0, len);
}

void drawLocalTransform(float len, float sw, PVector loc, 
  PVector xLocal, PVector yLocal, PVector zLocal) {
  strokeWeight(sw);
  stroke(0xffff0000);
  line(loc.x, loc.y, loc.z, 
    loc.x + xLocal.x * len, 
    loc.y + xLocal.y * len, 
    loc.z + xLocal.z * len);

  stroke(0xff00ff00);
  line(loc.x, loc.y, loc.z, 
    loc.x + yLocal.x * len, 
    loc.y + yLocal.y * len, 
    loc.z + yLocal.z * len);

  stroke(0xff0000ff);
  line(loc.x, loc.y, loc.z, 
    loc.x + zLocal.x * len, 
    loc.y + zLocal.y * len, 
    loc.z + zLocal.z * len);
}

PMatrix3D lookAt(PVector origin, PVector destination, 
  PVector refUp, PMatrix3D out, 
  PVector xLocal, PVector yLocal, PVector zLocal) {
  PVector.sub(destination, origin, yLocal);
  yLocal.normalize();

  PVector.cross(yLocal, refUp, xLocal);
  xLocal.normalize();

  PVector.cross(xLocal, yLocal, zLocal);
  zLocal.normalize();

  out.set(
    xLocal.x, yLocal.x, zLocal.x, origin.x, 
    xLocal.y, yLocal.y, zLocal.y, origin.y, 
    xLocal.z, yLocal.z, zLocal.z, origin.z, 
    0.0, 0.0, 0.0, 1.0);
  return out;
}

Note the orientation of the poles on Processing’s UV sphere depending on the coordinate systems used. That will influence how the texture appears on the final result.

2 Likes