TL; DR: https://youtu.be/6qNHy48iOOs
I've mentioned that I have (long) been working on a model of my house.
For some reason I re-obsessed today. I don't remember what got me
started, but I said "it would be nice to be able to make a walkthrough
animation".
Walkthrough is a bit awkward, because normal $vpr/$vpt is, you might
say, model-centric rather than camera-centric. Critically, simply
changing $vpr orbits the camera around the model, keeping it pointed at
$vpt, or rotates the model around $vpt, whichever way you want to look
at it. That's not what you want for a walkthrough; for a walkthrough
you want to rotate the camera while the model stays still.
Unsurprisingly, it turns out not to be very hard turn camera-centric
positions and rotations into $vpr/$vpt.
For a camera position pos and a look direction (theta and phi) dir, what
you need is:
$vpd = 140;
$vpt = pos + torect([$vpd, dir[0], dir[1]]);
$vpr = [180-dir[1], 0, (dir[0]+270)%360];
torect is a spherical-to-rectangular converter. It takes [rho, theta,
phi] and returns [x,y,z].
If you don't know [rho, theta, phi]:
https://en.wikipedia.org/wiki/Spherical_coordinate_system
Or, to type it correctly: [/r/, θ, φ]. Well, except rho is ρ;
apparently sometimes people are consistently Greek and sometimes they
mix English with Greek.
// Given a [rho, theta] or [rho, theta, phi], transform to
// an [x,y] or [x,y,z].
function torect(p) =
len(p) == 3 ? torect3(p) : torect2(p);
function torect2(p) = [
p[0] * cos(p[1]),
p[0] * sin(p[1])
];
function torect3(p) = [
p[0] * cos(p[1]) * sin(p[2]),
p[0] * sin(p[1]) * sin(p[2]),
p[0] * cos(p[2])
];
OK, now we have a way to position and turn a camera, in a camera-centric
way.
Astute readers will note that I don't yet have a way to roll the
camera. A roll angle would be a straightforward addition to rho,
theta, phi, and would undoubtedly involve setting the y-rotate
component of $vpr, but I haven't tried it yet and there might be
unpleasant details.
So let's make an animation out of it.
route = [
[0, [-230,-800,72], [90, 95]], // outside front door
[2, [-230,-150,72], [90, 95]], // into living room
[1, [-230,-150,72], [180, 100]], // look at fireplace
[.25, [-230,-150,72], [180, 100]], // pause a moment
[1, [-230,-150,72], [90, 95]], // turn back west
[1, [-230,25,72], [90, 95]], // forward to family room
[1, [-230,25,72], [0, 95]], // turn to parallel bar
[1, [-35,25,72], [0, 95]], // forward to kitchen entrance
[1, [-35,25,72], [-90, 95]], // turn towards kitchen
[1, [-35,-40,72], [-90, 95]], // enter kitchen
[1, [-35,-40,72], [-200, 100]], // turn to look at kitchen
[.25, [-35,-40,72], [-200, 100]], // pause a moment
[1, [-35,-40,72], [-90, 95]], // turn back east
[1, [-15,-180,72], [-90, 95]], // into dining room
[1, [-15,-180,72], [-180, 95]], // into dining room
[1, [-230,-180,72], [-180, 95]], // into dining room
[1, [-230,-180,72], [-90, 95]], // into dining room
[1, [-230,-300,72], [-90, 95]], // into dining room
];
The first element is the time for that particular movement, from the
previous position. (The units are relative; whatever they total up to
is the full range of the animation. The actual number of frames and
duration are set by the animation controls.) The second is the [x,y,z]
coordinate the camera is to move to. The third is the [theta, phi] that
the camera is to point at. Tracking around a curve exceeds my
cleverness right now, so all of these are either moves or turns.
(Subtlety: if you keep making turns in the same direction, you have to
keep going past 0 and 360; if you try to go from 359 to 1 a simple
mechanism (like the one below) will go the long way. You need to go
from 359 to 361.)
The times in that route are not very helpful, because you need to be
able to index $t into the array. Here's a perhaps horribly-inefficient
way to turn them into absolute times:
function routesum(a, start, end) = start > end ? 0 : a[start][0] + routesum(a, start+1, end);
r2 = [ for (i = [0:len(route)-1]) [ routesum(route, 0, i), route[i][1], route[i][2] ] ];
That will give us an array where the first element of each entry is [0,
2, 3, 3.25, 4.25, ...].
t_start = 0;
t_end = r2[len(r2)-1][0];
This says what subset of the animation to display, in time units. This
is "all of it".
positions = [ for (e = r2) [ e[0], e[1] ]];
directions = [ for (e = r2) [ e[0], e[2] ]];
I'm going to interpolate in a moment. I have an interpolator that will
do a triplet at a time, but not two triplets at a time.
pos = xyzinterp($t*(t_end-t_start)+t_start, positions);
dir = xyzinterp($t*(t_end-t_start)+t_start, directions);
As you will see in the comments for xyzinterp when I show it in a
moment, it seems like lookup() should be able to do this. But it
doesn't. (Or at least it didn't the last time I checked.)
So this gives us a camera position and direction for the current point
in the animation, and we come back to the magic lines I mentioned at the
top:
$vpd = 140;
$vpt = pos + torect([$vpd, dir[0], dir[1]]);
$vpr = [180-dir[1], 0, (dir[0]+270)%360];
(I haven't looked into what effect $vpd has. I suspect that
mathematically it doesn't have any effect, that all values yield the
same result.)
One thing that was helpful was to have a variation:
//$vpr=[0,0,0];
//translate(pos) color("red") {
// rotate(dir[0]) translate([0,-2,-1]) cube([10,4,2]);
// sphere(5);
//}
which has you watching down as a sphere with a nose walks through the
model. I suppose if I was really clever I'd set $vpt to pos, so that
the sphere stays in the center of the screen and the model moves past it.
Here's xyzinterp:
// Given a value and a table of value/position pairs, interpolate a
// position for that value.
// It seems like lookup() should do this sort of vector interpolation
// on its own, but it doesn't seem to.
function xyzinterp(v, table) =
let (x= [for (i=[0:len(table)-1]) [table[i][0], table[i][1][0]]])
let (y= [for (i=[0:len(table)-1]) [table[i][0], table[i][1][1]]])
let (z= [for (i=[0:len(table)-1]) [table[i][0], table[i][1][2]]])
[lookup(v, x), lookup(v, y), lookup(v,z)];
OK, so I fed that all to you in snippets. Here's a complete
self-contained demo. (I have this running in 2019.05.)
We're in a helicopter facing south. (You can tell because of the big
S.) We lift off, fly through the gate to the other side of the gate,
turn around, fly back through the gate, turn around to face south again,
and land.
// Animate a flythrough of a model.
// Jordan Brown 20 December 2020
// Public Domain. Go for it.
// First a couple of utility functions. These really belong in
// separate library files.
// Given a [rho, theta] or [rho, theta, phi], transform to
// an [x,y] or [x,y,z].
function torect(p) =
len(p) == 3 ? torect3(p) : torect2(p);
function torect2(p) = [
p[0] * cos(p[1]),
p[0] * sin(p[1])
];
function torect3(p) = [
p[0] * cos(p[1]) * sin(p[2]),
p[0] * sin(p[1]) * sin(p[2]),
p[0] * cos(p[2])
];
// Given a value and a table of value/position pairs, interpolate a
// position for that value.
// It seems like lookup() should do this sort of vector interpolation
// on its own, but it doesn't seem to.
function xyzinterp(v, table) =
let (x= [for (i=[0:len(table)-1]) [table[i][0], table[i][1][0]]])
let (y= [for (i=[0:len(table)-1]) [table[i][0], table[i][1][1]]])
let (z= [for (i=[0:len(table)-1]) [table[i][0], table[i][1][2]]])
[lookup(v, x), lookup(v, y), lookup(v,z)];
// Lay an array of cubes, to make it less confusing when we spin.
for (x=[-1000:100:1000], y=[-1000:100:1000]) translate([x,y,0]) cube(10);
// Put some tall ones at the corners, again to help you stay oriented.
for (x=[-1000,1000], y=[-1000,1000]) translate([x,y,0]) cube([10,10,100]);
// Put NSEW at the compass points.
for (e = [[90, "N"], [180, "W"], [270, "S"], [0, "E"]]) {
rotate(e[0]) translate([1000,0,0]) rotate([90,0,-90]) linear_extrude(height=1) text(e[1], size=100, halign="center");
}
// Finally, add a gate to fly through.
module gate() {
difference() {
translate([-50,-5,0]) cube([100,10,100]);
translate([0,0,50]) cube([80,12,80], center=true);
}
}
gate();
// Now that we have some terrain to fly through, let's fly!
// Let's start facing south through the gate, lift off,
// fly through the gate, turn around, fly back, turn around again,
// and land in our original position.
route = [
[0, [0,300,0], [270, 90]],
[1, [0,300,50], [270, 90]],
[1, [0,-300,50], [270, 90]],
[3, [0,-300,50], [90, 90]],
[1, [0,300,50], [90, 90]],
[2, [0,300,50], [-90, 90]],
[1, [0,300,0], [-90, 90]],
[1, [0,300,0], [-90, 90]]
];
// Convert our "relative" times above into absolute times.
// There might be a much more efficient way to do it; this one
// is quadratic on the number of steps in the animation.
function routesum(a, start, end) =
start > end
? 0
: a[start][0] + routesum(a, start+1, end);
r2 = [
for (i = [0:len(route)-1]) [
routesum(route, 0, i), route[i][1], route[i][2]
]
];
// Start and stop points. These are helpful if you want to work on
// a small part of a larger animation.
t_start = 0;
t_end = r2[len(r2)-1][0];
// Extract a list of times-and-positions
// and a list of times-and-look-directions.
positions = [ for (e = r2) [ e[0], e[1] ]];
directions = [ for (e = r2) [ e[0], e[2] ]];
// Get the current position and look direction.
pos = xyzinterp($t*(t_end-t_start)+t_start, positions);
dir = xyzinterp($t*(t_end-t_start)+t_start, directions);
// And here's where we make it happen.
$vpd = 140;
$vpt = pos + torect([$vpd, dir[0], dir[1]]);
$vpr = [180-dir[1], 0, (dir[0]+270)%360];
On 12/20/2020 10:44 PM, Jordan Brown wrote:
route = [
[0, [-230,-800,72], [90, 95]], // outside front door
[2, [-230,-150,72], [90, 95]], // into living room
[1, [-230,-150,72], [180, 100]], // look at fireplace
[.25, [-230,-150,72], [180, 100]], // pause a moment
[1, [-230,-150,72], [90, 95]], // turn back west
[1, [-230,25,72], [90, 95]], // forward to family room
[1, [-230,25,72], [0, 95]], // turn to parallel bar
[1, [-35,25,72], [0, 95]], // forward to kitchen entrance
[1, [-35,25,72], [-90, 95]], // turn towards kitchen
[1, [-35,-40,72], [-90, 95]], // enter kitchen
[1, [-35,-40,72], [-200, 100]], // turn to look at kitchen
[.25, [-35,-40,72], [-200, 100]], // pause a moment
[1, [-35,-40,72], [-90, 95]], // turn back east
[1, [-15,-180,72], [-90, 95]], // into dining room
[1, [-15,-180,72], [-180, 95]], // into dining room
[1, [-230,-180,72], [-180, 95]], // into dining room
[1, [-230,-180,72], [-90, 95]], // into dining room
[1, [-230,-300,72], [-90, 95]], // into dining room
];
Not that it matters but, sigh, I was copying and pasting entries to make
each additional entry, and never got around to fixing up the comments on
the last few. They should be "turn towards living room, forward to
living room, turn towards front door, leave house".