Part of the Advent of Grok {Shan, Shui}*. See blog post for intro and table of contents.

« Day 18 | Day 20 »

So… After yesterday philosophical explosion (sorry!), today I’ll continue to handle the mountain statements one-by-one.

Here, the weirdness waits. In he next statement, the tree-generation function, after some simplifications, looks like this (and produces the same old picture):

(x, y) => {
  var ht = ((height + y) / height) * 120 * rand(0.5, 1.0);
  var bc = rand(0.1)
  return Tree.tree03(x + x_offset, y + y_offset, {
    height: ht,
    bending: (x) => x * bc,
    color: treeColor(x, y),
  });
}

It seems obvious that ht and bc are not that precious names, so we might inline the calculations:

(x, y) => Tree.tree03(x + x_offset, y + y_offset, {
            height: ((height + y) / height) * 120 * rand(0.5, 1.0),
            bending: (x) => x * rand(0.1),
            color: treeColor(x, y),
          })

…right?

But this breaks everything!

Should be “randomness order” again, but I believe I never changed order of calls here… Oh, not so fast. In bending argument, we are actually passing a function, which tree03 will call several times, so the balance of randomness will change, after all!

So, the next tree will be (note we can’t inline tree_height calculation now—it would be calculated after bending_coef, which again will change order of randomness)…

//BOTTOM
generate(
  (x, y) => {
    var tree_height = ((height + y) / height) * 120 * rand(0.5, 1.0)
    var bending_coef = rand(0.1)
    return Tree.tree03(x + x_offset, y + y_offset, {
      height: tree_height,
      bending: (x) => x * bending_coef,
      color: treeColor(x, y),
    });
  },
  {if: (layerIdx, pointIdx, {layer}) =>
       (pointIdx == 0 || pointIdx == layer.length - 1) &&
       Noise.noise(layerIdx * 0.2, pointIdx * 0.05, seed)**4 < 0.012},
);

The condition here limits the tree to appear only at first or last points of the layer (plus, some random noise, as usual).

Next item, after some simplification/cleanup, looks like this (not a final version yet!):

//BOTT ARCH
generate(
  (x, y) => {
    switch(randChoice([0, 0, 1, 1, 1, 2])) {
    case 1:
      return Arch.arch02(x + x_offset, y + y_offset, seed, {
        width: normRand(40, 70),
        stories: randChoice([1, 2, 2, 3]),
        rotation: Math.random(),
        style: randChoice(['t_shapes', 'fence', 'grid']),
      });
    case 2:
      return Arch.arch04(x + x_offset, y + y_offset, seed, {sto: randChoice([1, 1, 1, 2, 2])});
    default:
      return "";
    }
  },
  {if: (layerIdx, pointIdx, {layer}) =>
       layerIdx != 0 && (pointIdx == 1 || pointIdx == layer.length - 2) &&
       Noise.noise(layerIdx * 0.2, pointIdx * 0.05, seed + 10)**4 < 0.008},
);

There are quite a few things of interest here.

First is normRand. What is normRand?

function normRand(m, M) {
  return mapval(Math.random(), 0, 1, m, M);
}
//...whilst mapval is defined as
function mapval(value, istart, istop, ostart, ostop) {
  return (
    ostart + (ostop - ostart) * (((value - istart) * 1.0) / (istop - istart))
  );
}

Oh. Compare it with the rand I once defined, too:

function rand(from, to=undefined) {
  if (to == undefined) {
    to = from;
    from = 0;
  }
  return Math.random() * (to - from) + from
}

So… They are basically the same. Seems like the original author at some point got tired of start + Math.random() * size non-atomicity too. Great minds think alike, right?..

Another interesting thing is that switch(randChoice([0, 0, 1, 1, 1, 2])): it splits probability in 6 part, and gives 2/6 probability of nothing would be drawn (0), 3/6 of arch02 (1), and 1/6 of arch04 (2).

We might try to spell it this way (with some invented syntax like probability(6, {1/6: ()=> ... arch02, 2/6: => ...arch04})), but that would be too awkward for pretty small gain.

So, after some consideration, I’ll just leave it be (replacing normRand with just rand for consistency).

Next:

//TOP ARCH
generate(
  (x, y) => Arch.arch03(x + x_offset, y + y_offset, seed, {sto: randChoice([5, 7]), wid: rand(40, 60)}),
  {if: (layerIdx, pointIdx, {layer}) =>
       layerIdx == 1 && Math.abs(pointIdx - layer.length / 2) < 1 && chance(0.02)},
);

Here, I can’t resist the temptation and replaced Math.random() > 0.02 with chance(0.02) (we already had a chance function, I just improved it a bit to allow this simple call).

Math.abs(pointIdx - layer.length / 2) < 1 is another case of the statement that both pretty trivial, but also “wait what”-causing a bit. What does it mean? My mathematically-lazy head needs some scratching. pointIdx goes from 0 to layer.length (which is actually always resolution.num_points = 50… So we might’ve used this number or that variable instead of passing the layer everywhere!). So, pointIdx - layer.length / 2 goes from -25 to +25. And indexes are integer. So, the only case when Math.abs(pointIdx - layer.length / 2) < 1 is true is… wait… pointIdx - 25 == 0, e.g. pointIdx == 25. Ugh.

//TOP ARCH
generate(
  (x, y) => Arch.arch03(x + x_offset, y + y_offset, seed, {sto: randChoice([5, 7]), wid: rand(40, 60)}),
  {if: (layerIdx, pointIdx) => layerIdx == 1 && pointIdx == resolution.num_points / 2 && chance(0.02)},
);

I needed to debug this revelation, but arch03 is actually quite rare. I changed its color to red… and nowhere on the picture it be seen. So, I temporarily removed the layerIdx == 1 && subcondition to have more of those. It would screw up the randomness, of course, but I need only compare this new (with screwed randomness) picture for staying the same when I replace Math.abs(pointIdx - layer.length / 2) < 1 with simpler version.

Well…. It does. For the reference, test picture looks like this:

Note that my simplification relies on the fact that num_points is known to be even (while the original statement will work with both even and odd); and using resolution instead of layer.length—which I’ll go back and replace everywhere now, actually—relies on the fact that all layers are of the same size. I suppose during the creative work of the original author they have tried many different approaches and varieties, and more “resilient” versions of functions totally make sense. So, in no way I am saying “the code was written bad, I rewrote it good”. The only idea behind what I do is to improve my understanding—and hopefully, an understanding of the curious reader—how this particular code might do what it does. And for that, some shortcuts are actually not that bad.

So, the rest of generate calls in the mountain function bring no new things to talk about:

//TRANSM
generate(
  (x, y) => Arch.transmissionTower01(x + x_offset, y + y_offset, seed),
  {if: (layerIdx, pointIdx) => layerIdx % 2 == 0 &&
                               (pointIdx == 1 || pointIdx == resolution.num_points - 2) &&
                               Noise.noise(layerIdx * 0.2, pointIdx * 0.05, seed + 20 * Math.PI)**4 < 0.002},
);

//BOTT ROCK
generate(
  (x, y) => Mount.rock(x + x_offset, y + y_offset, seed, {wid: rand(20, 40), hei: rand(20, 40), sha: 2}),
  {if: (layerIdx, pointIdx) => (pointIdx == 0 || pointIdx == resolution.num_points - 1) && chance(0.1)},
);

Tomorrow, I’ll finalize the mountain in general and comment on the final result. Sleeeeeep.

« Day 18 | Day 20 »