After I saw a video from the Coding Train on youtube about fractal trees, I tried to build one myself. Which worked great and I played with some variables to get different results.
I would love to see the tree moving like it got hit by some wind. I tried different approaches like rotating the branches a little bit or some minor physics implementations but that failed miserably.
So my question is: What would be the best approach to render a fractal tree and give it some sort of "life" like little shakes from wind. Is there some sort of good reference ? Do I need physics ? -> If so where do I have to look ? If not -> How could I fake such an effect?
I am glad about every help I can get.
Source for the idea: https://www.youtube.com/watch?v=0jjeOYMjmDU
The following are some short points re bending a branch in the wind. As the whole solution is complex you will have to get what you can from the code.
The code includes a seeded random number functions. A random recursive tree renderer, a poor quality random wind generator, all drawn on canvas using an animation loop.
To apply wind you need to add a bending force to each branch that is proportional to the angle of the branch to the wind.
So if you have a branch in direction dir
and a wind in the direct wDir
the amount of scaling the bending force needs is
var x = Math.cos(dir); // get normalize vector for the branch
var y = Math.sin(dir);
var wx = Math.cos(wDir); // get normalize vector for the wind
var wy = Math.sin(wDir);
var forceScale = x * wy - y * wx;
The length of the branch also effects the amount of force to include that you lengthen the vector of the branch to be proportional to its length
var x = Math.cos(dir) * length; // get normalize vector for the branch
var y = Math.sin(dir) * length;
var wx = Math.cos(wDir); // get normalize vector for the wind
var wy = Math.sin(wDir);
var forceScale = x * wy - y * wx;
Using this method ensures that the branches do not bend into the wind.
There is also the thickness of the branch, this is a polynomial relationship related to the cross sectional area. This is unknown so is scaled to the max thickness of the tree (an approximation that assumes the tree base can not bend, but the end branches can bend a lot.)
Then the elastic force of the bent branch will have a force that moves the branch back to its normal position. This acts like a spring and is very much the same as the wind force. As the computational and memory load would start to overwhelm the CPU we can cheat and use the wind to also recoil with a little bit of springiness.
The tree needs to be random, yet being fractal you don't want to store each branch. So you will also need a seeded random generator that can be reset at the start of each rendering pass. The tree is rendered randomly with each iteration but because the random numbers start at the same seed each time you get the same tree.
Draws random tree and wind in gusts. Wind is random so tree may not move right away.
Click tree image to reseed the random seed value for the tree.
I did not watch the video, but these things are quite standard so the recursive function should not be to far removed from what you may have. I did see the youTube cover image and it looked like the tree had no randomness. To remove randomness set the leng
, ang
, width
min, max to be the same. eg angMin = angMax = 0.4;
will remove random branch angles.
The wind strength will max out to cyclone strength (hurricane for those in the US) to see the max effect.
There are a zillion magic numbers the most important are as constants with comments.
const ctx = canvas.getContext("2d");
// click function to reseed random tree
canvas.addEventListener("click",()=> {
treeSeed = Math.random() * 10000 | 0;
treeGrow = 0.1; // regrow tree
});
/* Seeded random functions
randSeed(int) int is a seed value
randSI() random integer 0 or 1
randSI(max) random integer from 0 <= random < max
randSI(min, max) random integer from min <= random < max
randS() like Math.random
randS(max) random float 0 <= random < max
randS(min, max) random float min <= random < max
*/
const seededRandom = (() => {
var seed = 1;
return { max : 2576436549074795, reseed (s) { seed = s }, random () { return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max }}
})();
const randSeed = (seed) => seededRandom.reseed(seed|0);
const randSI = (min = 2, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
const randS = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
/* TREE CONSTANTS all angles in radians and lengths/widths are in pixels */
const angMin = 0.01; // branching angle min and max
const angMax= 0.6;
const lengMin = 0.8; // length reduction per branch min and max
const lengMax = 0.9;
const widthMin = 0.6; // width reduction per branch min max
const widthMax = 0.8;
const trunkMin = 6; // trunk base width ,min and max
const trunkMax = 10;
const maxBranches = 200; // max number of branches
const windX = -1; // wind direction vector
const windY = 0;
const bendability = 8; // greater than 1. The bigger this number the more the thin branches will bend first
// the canvas height you are scaling up or down to a different sized canvas
const windStrength = 0.01 * bendability * ((200 ** 2) / (canvas.height ** 2)); // wind strength
// The wind is used to simulate branch spring back the following
// two number control that. Note that the sum on the two following should
// be below 1 or the function will oscillate out of control
const windBendRectSpeed = 0.01; // how fast the tree reacts to the wing
const windBranchSpring = 0.98; // the amount and speed of the branch spring back
const gustProbability = 1/100; // how often there is a gust of wind
// Values trying to have a gusty wind effect
var windCycle = 0;
var windCycleGust = 0;
var windCycleGustTime = 0;
var currentWind = 0;
var windFollow = 0;
var windActual = 0;
// The seed value for the tree
var treeSeed = Math.random() * 10000 | 0;
// Vars to build tree with
var branchCount = 0;
var maxTrunk = 0;
var treeGrow = 0.01; // this value should not be zero
// Starts a new tree
function drawTree(seed) {
branchCount = 0;
treeGrow += 0.02;
randSeed(seed);
maxTrunk = randSI(trunkMin, trunkMax);
drawBranch(canvas.width / 2, canvas.height, -Math.PI / 2, canvas.height / 5, maxTrunk);
}
// Recusive tree
function drawBranch(x, y, dir, leng, width) {
branchCount ++;
const treeGrowVal = (treeGrow > 1 ? 1 : treeGrow < 0.1 ? 0.1 : treeGrow) ** 2 ;
// get wind bending force and turn branch direction
const xx = Math.cos(dir) * leng * treeGrowVal;
const yy = Math.sin(dir) * leng * treeGrowVal;
const windSideWayForce = windX * yy - windY * xx;
// change direction by addition based on the wind and scale to
// (windStrength * windActual) the wind force
// ((1 - width / maxTrunk) ** bendability) the amount of bending due to branch thickness
// windSideWayForce the force depending on the branch angle to the wind
dir += (windStrength * windActual) * ((1 - width / maxTrunk) ** bendability) * windSideWayForce;
// draw the branch
ctx.lineWidth = width;
ctx.beginPath();
ctx.lineTo(x, y);
x += Math.cos(dir) * leng * treeGrowVal;
y += Math.sin(dir) * leng * treeGrowVal;
ctx.lineTo(x, y);
ctx.stroke();
// if not to thing, not to short and not to many
if (branchCount < maxBranches && leng > 5 && width > 1) {
// to stop recusive bias (due to branch count limit)
// random select direction of first recusive bend
const rDir = randSI() ? -1 : 1;
treeGrow -= 0.2;
drawBranch(
x,y,
dir + randS(angMin, angMax) * rDir,
leng * randS(lengMin, lengMax),
width * randS(widthMin, widthMax)
);
// bend next branch the other way
drawBranch(
x,y,
dir + randS(angMin, angMax) * -rDir,
leng * randS(lengMin, lengMax),
width * randS(widthMin, widthMax)
);
treeGrow += 0.2;
}
}
// Dont ask this is a quick try at wind gusts
// Wind needs a spacial component this sim does not include that.
function updateWind() {
if (Math.random() < gustProbability) {
windCycleGustTime = (Math.random() * 10 + 1) | 0;
}
if (windCycleGustTime > 0) {
windCycleGustTime --;
windCycleGust += windCycleGustTime/20
} else {
windCycleGust *= 0.99;
}
windCycle += windCycleGust;
currentWind = (Math.sin(windCycle/40) * 0.6 + 0.4) ** 2;
currentWind = currentWind < 0 ? 0 : currentWind;
windFollow += (currentWind - windActual) * windBendRectSpeed;
windFollow *= windBranchSpring ;
windActual += windFollow;
}
requestAnimationFrame(update);
function update() {
ctx.clearRect(0,0,canvas.width,canvas.height);
updateWind();
drawTree(treeSeed);
requestAnimationFrame(update);
}
body {
font-family : arial;
}
<canvas id="canvas" width="250" heigth="200"></canvas>
Click tree to reseed.
Update
I just noticed that the wind and branch length are absolute thus drawing the tree on a larger canvas will create a bending force too great and the branches will bend past the wind vector.
To scale the sim up either do it via a global scale transform, or reduce the windStrength
constant to some smaller value. You will have to play with the value as its a 2nd order polynomial relation. My guess is multiply it with (200 ** 2) / (canvas.height ** 2)
where the 200 is the size of the example canvas and the canvas.height
is the new canvas size.
I have added the calculations to the example, but its not perfect so when you scale you will have to change the value windStrength
(the first number) down or up if the bending is too far or not enough.