How would I rotate a vector in 3D space? P5.js
Asked Answered
K

1

5

This is actually my first question on stackoverflow :)

Firstly, I wanted to say that I am new to coding and I am wanting to get an understanding of some syntax. Basically, I am following a tutorial by the codetrain - creating an object orientated fractal tree, I am wondering based on the code below, how I would make it more '3D'. One way I thought of this, is through rotating the vectors. I have been looking for potential solutions to rotating the following vector in 3D space. The example codetrain uses is to make a direction vector: direction = p5.Vector.Sub(this.begin, this.end). He then uses the code direction.rotate(45). I realise that you can't write direction.rotateY(45). I saw from the p5.js syntax that .rotate() can only be used for 2D vectors. Is there, therefore, an syntax that I have somehow overlooked that rotates the vector in 3D space based on the following code?

Here is the code for the sketch. The Branch class controls tree construction, and the branchA and branchB function are where some rotation might be added.

var tree = [];
var leaves = [];
var count = 0;

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  angleMode(DEGREES)

  var randomangle = random(0, 90)
  var randomMinusAngle = random(0, -90)

  var a = createVector(0, 0, 0);
  var b = createVector(0, -100, 0);

  var root = new Branch(a, b);
  tree[0] = root;
}

//INPUT.

function mousePressed() {
  for (var i = tree.length - 1; i >= 0; i--) {
    if (!tree[i].finished) {
      tree.push(tree[i].branchA())
      tree.push(tree[i].branchB())
    }

    tree[i].finished = true;
  }

  count++
  if (count === 8) {
    for (var i = 0; i < tree.length; i++) {
      if (!tree[i].finished) {

        var leaf = tree[i].end.copy();

        leaves.push(leaf);
      }
    }
  }
}

function draw() {
  background(51);
  //orbitControl();
  rotateY(frameCount);
  translate(0, 200, 0);
  
  for (var i = 0; i < tree.length; i++) {
    tree[i].show();
    tree[i].rotateBranches();

    //tree[i].jitter();
  }

  //LEAVES
  for (var i = 0; i < leaves.length; i++) { //should  make the leaves an object in new script.
    fill(255, 0, 100);
    noStroke();
    ellipse(leaves[i].x, leaves[i].y, 8, 8);
  }
}

function Branch(begin, end) {
  this.begin = begin;
  this.end = end;

  //bool to check whether it has finished spawning branch.
  this.finishedGrowing = false;

  this.show = function() {
    stroke(255);

    line(this.begin.x, this.begin.y, this.begin.z, this.end.x, this.end.y, this.end.z);
  }

  //var rotateValue = random(0, 45)

  this.branchA = function() {
    var direction = p5.Vector.sub(this.end, this.begin);

    direction.rotate(45)
    direction.mult(0.67);

    var newEnd = p5.Vector.add(this.end, direction);

    var randomss = p5.Vector.random3D(this.end, newEnd)
    var b = new Branch(this.end, newEnd);

    return b;
  }
  
  this.branchB = function() {
    var direction = p5.Vector.sub(this.end, this.begin)

    direction.rotate(-15);

    direction.mult(0.67);
    var newEnd = p5.Vector.add(this.end, direction);

    var b = new Branch(this.end, newEnd);

    return b;
  }
  
  this.rotateBranches = function() {
    // TODO
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
Kayleigh answered 9/5, 2021 at 14:3 Comment(1)
Great question. I had fun answering this. Hopefully it is helpful.Sapindaceous
S
8

You're not missing anything. Unfortunately p5js does not provide a way to rotate a p5.Vector in 3d (the rotateX/rotateY/rotateZ functions referred to in the other answer transform the view matrix which changes where in world space primitives are draw, but that is not the same thing, although it can also be used to draw lines in different 3d orientations). However, you can implement this yourself. There are a couple of ways to do 3d rotations: you can use Euler angles, you can use transformation matrices, you can use Quaternions. However, the method I find most easy to understand is axis-angle representation (Quaternions are actually just a special way to encode axis-angle rotations such that they can be manipulated and applied efficiently).

In axis-angle representation, you rotate a vector by specifying an axis about which to rotate it, and an angle indicating how far to rotate.

In this example gif the green arrow represents the axis vector, and the purple arrow represents the vector being rotated (in this case it happens to pass through each axis of the coordinate system).

While it may not be the most efficient, there is a simple algorithm to compute such a rotation called Rodrigues' rotation formula. Given a vector v and an axis vector k (which must be a unit vector, meaning it has a length of 1), the formula is:

Note: is the symbol for vector cross product, and · is the symbol for vector dot product. If you aren't familiar with these it's not too important. Just know that they are ways of combining two vectors mathematically, and cross product produces a new vector, while dot product produces a numeric value. Here is the formula implemented with p5.js:

// Rotate one vector (vect) around another (axis) by the specified angle.
function rotateAround(vect, axis, angle) {
  // Make sure our axis is a unit vector
  axis = p5.Vector.normalize(axis);

  return p5.Vector.add(
    p5.Vector.mult(vect, cos(angle)),
    p5.Vector.add(
      p5.Vector.mult(
        p5.Vector.cross(axis, vect),
        sin(angle)
      ),
      p5.Vector.mult(
        p5.Vector.mult(
          axis,
          p5.Vector.dot(axis, vect)
        ),
        (1 - cos(angle))
      )
    )
  );
}

Ok, so armed with this, how do we apply it to the sketch in question. Currently the branch direction vectors are always flat in the XY plane and are always rotated around the Z axis when forking. When we start letting the branches move into the Z dimension this gets a bit more complicated. Firstly, when forking we need to find an initial axis of rotation that is perpendicular to the current branch. We can do this using cross product (see the findPerpendicular helper function). Once we have that axis we can perform our fixed "fork" rotation of either 45 or -15. Next we "twist" the new branch direction vector around the original branch direction, and this is what gets our branches into the Z dimension.

let tree = [];

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  angleMode(DEGREES);

  const start = createVector(0, 0, 0);
  const end = createVector(0, -100, 0);

  tree[0] = new Branch(start, end);
}

function mousePressed() {
  // Add another iteration of branches
  for (let i = tree.length - 1; i >= 0; i--) {
    if (!tree[i].finished) {
      // Pick a random twist angle for this split.
      // By using the same angle we will preserve the fractal self-similarity while
      // still introducing depth. You could also use different random angles, but
      // this would produce some strange un-tree-like shapes.
      let angle = random(-180, 180);
      tree.push(tree[i].branchA(angle));
      tree.push(tree[i].branchB(angle));
    }

    tree[i].finished = true;
  }
}

function draw() {
  background(51);
  //orbitControl();
  rotateY(frameCount);
  translate(0, 200, 0);

  // Show all branches
  for (let i = 0; i < tree.length; i++) {
    tree[i].show();
  }
}

function Branch(begin, end) {
  this.begin = begin;
  this.end = end;

  //bool to check whether it has finished spawning branch.
  this.finishedGrowing = false;

  this.show = function() {
    stroke(255);

    line(this.begin.x, this.begin.y, this.begin.z, this.end.x, this.end.y, this.end.z);
  }

  this.branch = function(forkAngle, twistAngle) {
    let initialDirection = p5.Vector.sub(this.end, this.begin);

    // First we rotate by forkAngle.
    // We can rotate around any axis that is perpendicular to our current branch.
    let forkAxis = findPerpendicular(initialDirection);

    let branchDirection = rotateAround(initialDirection, forkAxis, forkAngle);

    // Next, rotate by twist axis around the current branch direction.
    branchDirection = rotateAround(branchDirection, initialDirection, twistAngle);
    branchDirection.mult(0.67);

    let newEnd = p5.Vector.add(this.end, branchDirection);
    return new Branch(this.end, newEnd);
  }

  this.branchA = function(twistAngle) {
    return this.branch(45, twistAngle);
  }

  this.branchB = function(twistAngle) {
    return this.branch(-15, twistAngle);
  }
}

function findPerpendicular(vect) {
  const xAxis = createVector(1, 0, 0);
  const zAxis = createVector(0, 0, 1);
  // The cross product of two vectors is perpendicular to both, however, the
  // cross product of a vector and another in the exact same direction is
  // (0, 0, 0), so we need to avoid that.
  if (abs(vect.angleBetween(xAxis)) > 0) {
    return p5.Vector.cross(xAxis, vect);
  } else {
    return p5.Vector.cross(zAxis, vect);
  }
}

function rotateAround(vect, axis, angle) {
  axis = p5.Vector.normalize(axis);
  return p5.Vector.add(
    p5.Vector.mult(vect, cos(angle)),
    p5.Vector.add(
      p5.Vector.mult(
        p5.Vector.cross(axis, vect),
        sin(angle)
      ),
      p5.Vector.mult(
        p5.Vector.mult(
          axis,
          p5.Vector.dot(axis, vect)
        ),
        (1 - cos(angle))
      )
    )
  );
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
Sapindaceous answered 10/5, 2021 at 10:8 Comment(1)
Wow, this answer is incredibly helpful - not just for solving the problem, but also because now I can sort of understand how you can approach vector issues such as this. Really thorough thanks! I learned a lot.Kayleigh

© 2022 - 2024 — McMap. All rights reserved.