December 2, 2013

Tile-Based Discrete Wavefront Propagation

I'm currently building a very simplistic first-person shooter in WebGL. An example of this algorithm, with the debug code left in, is available here. The map is represented by a grid - a cell is solid if its 0, and empty if its 1. This is trivial to render by using a standard recursive 4-direction flood fill algorithm. Unfortunately, we can't simply render the entire level if our rooms contain high levels of detail or many objects, because we'll overload the GPU.

Frustum culling is the obvious answer, but I need it to be highly efficient. This means adjusting the frustum to account for corners, so I only render the visible portions of the level. While a lot of the speed concerns I currently have can be alleviated using batch rendering, this will stop working the instant I put in details that can't be batch rendered. Consequently, I want the algorithm to be as close to perfect as possible, only rendering rooms that are visible and no others.

This is where Tile-Based Discrete Wavefront Propagation comes in. The central idea is to represent the viewing frustum as a wave that flows through the level, getting split up when it hits corners. If properly implemented, this will be exact, only rendering rooms that are visible and no others. It turns out that you can implement this in $$O(n)$$ time, but we can't use the naïve recursive flood-fill anymore, because it's depth first. If we want to simulate a wave, we need to render the grid using a breadth-first search, to simulate it slowly spreading outward. Breadth first search is implemented using a queue by simply pushing all neighboring nodes on to the queue and rendering them until the queue is empty.

As the wave propagates through the level, we adjust the left and right angles of each individual wavefront according to the walls that we hit. This requires that we deal with two possible cases: the case where a wall is in front of the wave as it propagates through the level, and the case where it isn't. The only time the wave can actually be split up is when a wall is in front of it - the rest of the time, it is simply clipped on the sides. "Front" and "side" are defined based on what direction the wave came from.

Starting with the case where there isn't a wall in front, we check to see if there is a wall on the right. If there is, we check the angles of it's corners. If either angle is inside the culling frustum, we change the right-angle to the one that is "farthest inside" (done in code by simply checking each angle one after the other). Then we do the same for the left wall, this time changing the left-angle, if appropriate. Then we send the updated angles to the neighboring tiles - the left one gets (left-angle,new-right-angle), the front gets (new-left-angle,new-right-angle), and the right gets (new-left-angle,right-angle).

If there is a wall in front, everything stays the same (note that the front wall has it's own set of corners you must check), but the angles that get sent through the neighboring tiles change. The left wall gets (left-angle,new-left-angle), the front doesn't exist so we ignore it, and the right wall gets (new-right-angle,right-angle). This effectively splits the wave down the middle, but it makes things complicated. Before, if a new left-angle was outside the culling frustum, we would just set it to the old left-angle. This doesn't work anymore, because we're splitting the wave, which means we require a new left-angle that isn't equal to the old left-angle. To solve this, if the new left-angle fails to exist, we set it to the old right-angle instead of the old left-angle, and vice-versa for the new right-angle.

Conceptually, this isn't too complicated, but the actual implementation gets complicated very fast due to angles being a giant pain in the ass to work with. We have to seed the initial queue with the neighboring tiles, and deal with getting proper frustum culling working, which is far more difficult than it seems. This will require a sizable number of utility functions, so we'll look at those first:

function realmod(i,m) { i=(i%m); return i+((i<0)*m); } // Mathematically correct modulo
// Queue implementation using a circular array. Resize is very expensive, but almost never happens, because it remembers its size after each frame.
function Queue(sz) {
  this.array = new Array(!sz?1:sz);
  this.cur=-1;
  this.length=0;
  this.push = function() {
    for (var i = 0; i < arguments.length; i++) {
      if(this.length>=this.array.length) {
        this.resize(this.array.length*2);
      }
      this.cur=((this.cur+1)%this.array.length);
      this.array[this.cur] = arguments[i];
      this.length += 1;
    }
  }
  this.pop = function() { return this.get(--this.length); }
  this.peek = function() { return this.get(this.length-1); }
  this.get = function(i) { return this.array[this.modindex(i)]; }
  this.clear = function() { this.cur=-1; this.length=0; }
  this.resize = function(nsize) { 
    var sz=this.array.length;
    var c = this.cur+1;
    for(var i=sz-c; (i--)>0;) {
      this.array[c+nsize-sz+i]=this.array[c+i];
    }
  }
  this.modindex = function(i) { return realmod(this.cur-i,this.array.length); }
}
function realfmod(x,m) { return x - Math.floor(x/m)*m } // Implements mathematically correct fmod
// Gets absolute distance between two angles
function getAngleDist(u,v) { return Math.PI - Math.abs((Math.abs(u - v)%(Math.PI*2)) - Math.PI); } 
// Gets the signed distance between two angles
function getAngleDistSign(u,v) { return ((realfmod(v - u,Math.PI*2) + Math.PI)%(Math.PI*2)) - Math.PI; } 

  function getdx(dir) { dir=((dir+4)%4); return (dir==0)-(dir==2); }
  function getdy(dir) { dir=((dir+4)%4); return (dir==1)-(dir==3); }
  function exists(x,y,m,w) { var i = x + (y*w); return x>=0 && x<w && i<m.length && (m[i]&1)!=0; }
  function exists_dir(x,y,m,w,dir) { return exists(x+getdx(dir),y+getdy(dir),m,w); }

  // Gets the angle of a point from the player's origin (which is the camera's origin)
  var getangle = function(x,y) { return Math.atan2(y-player[0].elements[2],x-player[0].elements[0]); }
  // Gets the angle of a corner, offset from the given point by dir and diri.
  var getangle_dir = function(x,y,dir,diri) { return getangle(x+getdx(dir)*5 + getdx(diri)*5,y+getdy(dir)*5 + getdy(diri)*5); }

First, the modulo operators in most programming languages are actually remainder operators. They do not perform mathematically correct modulo, and their behavior when you feed them negative numbers is not what you'd expect. The first thing we do, then, is define a modulo function that actually behaves like the modulo operator. We then use this to build a circular queue that resizes itself when necessary. When resizing, the queue must move all items to the right of the index over, which is a costly operation, so we let it keep it's size across frames. This makes the number of resize operations essentially zero.

The next few functions deal with angles. Angles are inherently circular, and this causes all sorts of problems. If our left-angle is 355°, and our right angle is 5°, the distance between these two angles is 10°, not 350°. There is a standard method to getting the absolute distance between two angles using the floating point modulo operator, which is implemented in getAngleDist(). This is all well and good, but we have defined left and right angles, which means we need to know if something is on the left hand side, or the right hand side. This requires a signed angular distance function, which makes things much more complicated because, once again, the floating point modulo operator is not actually modulo, it's a remainder.

So, we need to implement a proper floating point modulo operator. The standard fmod() function is implemented using the following formula:\[ \DeclareMathOperator{\fmod}{fmod} \DeclareMathOperator{\trunc}{trunc} \fmod(n,d) = n- \trunc\left(\frac{n}{d}\right) d\]It's trivial to change this formula into one that gives us the correct behavior (remember, truncation is not flooring!):
\[ \fmod(n,d) = n- \left\lfloor\frac{n}{d}\right\rfloor d\]We can use this to define a function such that, as long as $$b-a$$, when forced into the range $$\left[0,2\pi\right)$$, is less than $$\pi$$ (or 180°), we get a positive distance. If it goes past $$\pi$$, it becomes negative and heads back towards 0, just like in the absolute value distance function, but now with the proper sign. We will use this later to determine if a tile crosses over our angular culling frustum. getdx(),getdy(),exists(), and exists_dir() are all used to either bump x/y coordinates according to a direction, or determine if a specific tile exists. Now we get to the real function:

var roomq = new Queue(10); // queue holding nodes
var drawmap = function(x,y,m,w,langle,rangle) { // map drawing function
  var i = x + (y*w);
  m[i]=(m[i]|2);
  // draw floor
     
  for(var j = 0; j < 4; ++j) { // Push initial neighbors on to the queue
    var nx=x+getdx(j);
    var ny=y+getdy(j);
    if(!exists(nx,ny,m,w)) { // If it doesn't exist, we ran off the edge of the map or hit a wall
      // draw wall
    } else {
      roomq.push(nx,ny,langle,rangle,j);
    }
  }
  
  var dir=0; // Stores the direction the wave is going. 0: (+) x-axis, 1: (+) y-axis, 2: (-) x-axis, 3: (-) y-axis
  while(roomq.length>0) {
    x = roomq.pop();
    y = roomq.pop();
    langle = roomq.pop();
    rangle = roomq.pop();
    dir = roomq.pop();
    var i = x + (y*w);
    var room=m[i];
    if((room&2)==2) continue; // If true, we already visited this node
    var mid = [langle,getAngleDistSign(langle,rangle)/2];
    if(mid[1]<0) continue; // If this is less than 0, langle has cross over rangle, so there's nothing to render.
    mid[0]=langle+mid[1];
    var angles=[getAngleDistSign(getangle(x*10+5,y*10+5),mid[0]),
                getAngleDistSign(getangle(x*10-5,y*10-5),mid[0]),
                getAngleDistSign(getangle(x*10-5,y*10+5),mid[0]),
                getAngleDistSign(getangle(x*10+5,y*10-5),mid[0])];
    if(angles[0]>mid[1] && angles[1]>mid[1] && angles[2]>mid[1] && angles[3]>mid[1]) continue;
    if(angles[0]<-mid[1] && angles[1]<-mid[1] && angles[2]<-mid[1] && angles[3]<-mid[1]) continue;
    if(Math.abs(angles[0])>Math.PI/2 && Math.abs(angles[1])>Math.PI/2 && Math.abs(angles[2])>Math.PI/2 && Math.abs(angles[3])>Math.PI/2) continue;
    m[i]=(m[i]|2); // Only mark as done if we actually render it. This let's us recover from inconsistencies.
    // Draw floor
    var nlangle=langle;
    var nrangle=rangle;
    var front = exists_dir(x,y,m,w,dir); // Is there a wall in front of us?
    var lwall = exists_dir(x,y,m,w,dir-1); // To the left?
    var rwall = exists_dir(x,y,m,w,dir+1); // To the right?
    
    if(!front || !lwall) { //left wall or front wall check
      nlangle=getangle_dir(x*10,y*10,dir,dir-1);
      if(getAngleDist(mid[0],nlangle)>mid[1]) nlangle=(!front)?rangle:langle; 
    }
    if(!lwall) { // This corner is only checked for left walls
      nlangle=getangle_dir(x*10,y*10,dir-2,dir-1);
      if(getAngleDist(mid[0],nlangle)>mid[1]) nlangle=langle; 
    }
    if(!front || !rwall) { //right wall or front wall check
      nrangle=getangle_dir(x*10,y*10,dir,dir+1);
      if(getAngleDist(mid[0],nrangle)>mid[1]) nrangle=(!front)?langle:rangle;
    }
    if(!rwall) { // Only right wall
      nrangle=getangle_dir(x*10,y*10,dir-2,dir+1);
      if(getAngleDist(mid[0],nrangle)>mid[1]) nrangle=rangle;
    }
    
    for(var j = dir-1; j <= dir+1; ++j) {
      var k = (j+4)%4; // get proper direction
      var nx=x+getdx(k);
      var ny=y+getdy(k);
      if(!exists(nx,ny,m,w)) { // We ran off the edge of the map or hit a nonexistent block
        // Draw wall
      } else {
        if(j==dir-1) { roomq.push(nx,ny,langle,(!front)?nlangle:nrangle,k); }
        else if(j==dir) { roomq.push(nx,ny,nlangle,nrangle,k); }
        else { roomq.push(nx,ny,(!front)?nrangle:nlangle,rangle,k); }
      }
    }      
  }
}
var reversefill = function(x,y,m,w) {
  var i = x + (y*w);
  if(x<0 || x>=w || i>=m.length) return;
  if((m[i]&2)==2)
  {
    m[i]=(m[i]&(~2));
    reversefill(x+1,y,m,w);
    reversefill(x,y+1,m,w);
    reversefill(x-1,y,m,w);
    reversefill(x,y-1,m,w);
  }
}

Straddle Error
First, we push our 4 neighboring tiles, provided they exist, on to the queue, seeding them with appropriate directions that radiate outward from our starting point. Then, we start running through the queue, and do an initial check to see if we already dealt with this tile. If not, we check to make sure that the left-angle is really less than the right-angle, and then go into standard frustum culling. In order to figure out if a tile is within our view, we need to ensure that the entire tile is either to the right or to the left of our angular viewing frustum. This means that all 4 corners must have angles outside of our frustum, and they must all be on the same side. If they are not on the same side, then the cone could have passed through the center.

This is what the first two if statements do. The problem is that a tile directly behind us will also be considered straddling the angular range, because our distance function wraps at 180°. To solve this, we have a third if statement that throws away everything that is more than 90° (or $$\frac{\pi}{2}$$) from the center of our frustum. This means the maximum horizontal field-of-view is 180°. Note that this culling is independent of the algorithm, you'd be using this same logic if you were only doing simple frustum culling.


Split Error
If the tile survived the culling function, we mark it as visited. It's important that we only mark a node as visited after we have decided to render it because of a very nasty edge-case that can happen during the breadth-first traversal. It's possible for a tile that is rendered on one side of a split frustum to reach a visible tile on the other side of the split, and erronously mark it as invisible, using it's own frustum. The tile actually is visible, but only to the frustum on the other side of the split.

Then, we check the appropriate corners and assign langle and rangle appropriately. The neighbors are iterated and new values passed as needed. Once this phase of the algorithm is completed, we call reversefill(), starting on the same tile we called drawmap() with. This will use the standard fill algorithm to reset the "visited" marks on the map. By doing this, we avoid having to set all 2500 nodes of a 50x50 map to 0. The algorithm is now complete.

Because this algorithm runs in $$O(n)$$ time and is exact, it is asymptotically optimal. The problem is that it is only optimal in a single-threaded sense. The breadth-first iteration technique allows us to run each individual level concurrently, but this will only reduce the worst-case complexity from $$O(mn)$$ to $$O(\max(m,n))$$. 50 is a lot better than 2500, but this is only for a very, very large room. If we're dealing with a hallway, the concurrent performance would be identical to the single-threaded performance!

Consequently, while this is useful for tight corridors, the algorithm itself isn't really that useful outside of the game's narrow domain. Once the rooms start getting large enough, you're probably better off brute-forcing most of it and using approximations. However, the algorithm is exact, which is very important for calculating Enemy Line-of-Sight. So if you're dealing with a grid-based game and need to figure out if an enemy has a direct line of sight, this technique allows you to make a precise calculation, since it only hits tiles that are visible, and no others. This gives it an advantage over naïve raytracing, which requires many rays and is prone to giving false-negatives when dealing with narrow hallways.

Unfortunately, it still breaks when you try to make it work with FoV's greater than 180°, so unless you split them up into 90° chunks, it's pretty useless for things like lighting. I'm probably not the first to come up with the algorithm, either; someone probably invented this in their garage in 1982. Oh well.

November 17, 2013

Google's Decline Really Bugs Me

Google is going down the drain.

That isn't to say they aren't fantastically successful. They are. I still use their products, mostly because I don't put things on the internet I don't want other people to find, and I'm not female, so I don't have to worry about misogynists stalking me. They still make stupendous amounts of money and pump out some genuinely good software. They still have the best search engine. Like Microsoft, they'll be a force to be reckoned with for many decades to come.

Google, however, represented an ideal. They founded the company with the motto "Don't Be Evil", and the unspoken question was, how long would this last? The answer, oddly enough, was "until Larry Page took over".

In its early years, Google unleashed the creativity of the brilliant people it hired to the world and came up with a slew of fantastic products that were a joy to use. Google made huge contributions to the open-source world and solved scalability problems with an elegance that has yet to be surpassed. They famously let engineers use 20% of their time to pursue their own interests, and the result was an unstoppable tidal wave of innovation. Google was, for a brief moment, a shining beacon of hope, a force of good in a bleak world of corporations only concerned with maximizing profit.

Then Larry Page became CEO. Larry Page worshiped Steve Jobs, who gave him a bunch of bad advice centered around maximizing profit. The result was predictable and catastrophic, as the entire basis of what had made Google so innovative was destroyed for the sake of maximizing profit. Now it's just another large company - only concerned about maximizing profit.

Google was a company that, for a time, I loved. To me, they represented the antithesis of Microsoft, a rebellion against a poisonous corporate culture dominated by profiteering that had no regard for its users. Google was just a bunch of really smart people trying to make the world a better place, and for a precious few years, they succeeded - until it all came tumbling down. Like an artist whose idol has become embroiled in a drug abuse scandal, I have lost my guiding light.

Google was largely the reason I wanted to start my own company, even if college kept me from doing so. As startup culture continued to suck the life out of silicon valley, I held on to Google as an ideal, an example of the kind of company I wanted to build instead of a site designed to sort cat photos. A company that made money because it solved real problems better than everyone else. A company that respected good programming practices, using the right tool for the job, and the value of actually solving a problem instead of just throwing more code at it.

Google was a company that solved problems first, and made money second.

Now, it has succumbed to maximizing stock price for a bunch of rich wall street investors who don't care about anything other than filling their own pockets with as much cash as they possibly can. Once again, the rest of the world is forced to sit around, waiting until an investor accidentally makes the world a better place in the process of trying to make as much money as possible.

Most people think this is the only way to get things done. For a precious few years, I could point to Google and say otherwise. Now, it has collapsed, and its collapse has made me doubt my own resolve. If Google, of all companies, couldn't maintain that idealistic vision, was it even possible?

Google gave me a reason to believe that humanity could do better. That we could move past a Wall Street that has become nothing more than a rotting cesspool of greed and corruption.

Now, Google has fallen, along with the ideal it encompassed. Is there a light at the end of the tunnel? Or is it a train, a force of reality come to remind us that no matter how much we reach for utopia, we will be sentenced to drown in our own greed?

October 29, 2013

The Educational Imbroglio

im·bro·glio
noun
1. an extremely confused, complicated, or embarrassing situation.

Across the country, there is a heated debate over our educational system. Unfortunately, it's a lot like watching members of the flat earth society argue about whose theory is right - there is no right answer, because everyone is wrong.

The most recent and perhaps egregious example of this is a breathtakingly misguided article by Kevin G. Welner, who is the director of the National Education Policy center, which is absolutely terrifying. He is attempting to discredit the idea of "tracking", wherein low-performing students are separated from higher achieving students, because obviously you can't teach kids who "get it" the same way as kids who are struggling. Unfortunately, the entire conclusion rests on a logical fallacy. He says, and I quote:
"When children fall behind academically, we have a choice. We can choose to sort them into less demanding classes where they will fall further behind, or we can choose to include them in classes that maintain high expectations."
This is a false dichotomy, since there are many other choices. We can sort them into a class with the same expectations, but an alternative teaching method. Sort them into a class that actually pays attention to the concepts that are giving them trouble. The idea is to help children who have fallen behind catch up with their peers, not throw them in a room and forget about them. Schools that simply lower their expectations of poorly performing students are doing it wrong. Furthermore, trying to argue that something can't work because no one's doing it properly is another logical fallacy.

There's also a persistent argument against charter schools, which claims that the money spent on charter schools should instead be used to improve public schools instead. This is laughable, because public schools receive funding based on test scores. So, all the money would be spent improving test scores instead of actually teaching children anything. Charter schools are important because they aren't bounded by these nonsensical restrictions and thus are free to experiment with alternative teaching styles. Throwing money at our public schools will only shore up a method of teaching that's fundamentally broken. It's like trying to fix the plumbing after the roof caved in - it's completely missing the point.

To make matters worse, there's also a war on free time. Recess is being cut in favor of increased instruction time, while educators cite minuscule increases in test scores as proof that this "works". If by "works", you mean it succeeds in cramming more useless junk into kids heads, then sure, it's "working". However, if you want kids to actually learn instead of memorize pointless facts that they will immediately forget, you have to give them time to process concepts. Their brains need rest, not more work. Bodybuilders don't lift weights as long as they can every single day; they lift weights every other day and only for a few hours or so, because the muscle needs time to recover.

This, however, is an issue with a society that thinks hard work means working yourself to exhaustion. This is incredibly short-sighted and in direct opposition to plenty of evidence that points to rest being a necessary part of a healthy lifestyle. It can be your job, or school, or a hobby, it doesn't matter. Humans do not function effectively when forced to focus on one thing for hours at a time. The only reason we attempt to do this is because we used to work in factories, but nowadays we have robots. Modern jobs are all about thinking creatively, which cannot be forced. You can't force yourself to understand a concept. It's like trying to force a broken leg to heal faster by going for a jog. You must give kids time absorb concepts instead of trying to cram it down their throats. They need to understand what they are learning, not memorize formulas.

Mainstream education doesn't take this seriously. There are plenty of experiments that have effectively taught children advanced concepts with radically different teaching methods. One guy taught 3rd graders binary. These kids learned english and how to operate a computer without a teacher at all. There are plenty of cases that show just how woefully inadequate our teaching system is, but it seems that we care more about a one-size-fits-all method that can be mass-produced than a method that's actually effective.

Our educational system is failing our students, and we refuse to even entertain notions that could make a difference. Instead, we just legislate more tests and take away their recess.

Because really, memorizing the date of the Battle of Gettysburg is more important than playing outside and having a childhood.

October 3, 2013

The Ladder-Climbing Generation

Creating a life that reflects your values and satisfies your soul is a rare achievement. In a culture that relentlessly promotes avarice and excess as the good life, a person happy doing his own work is usually considered an eccentric, if not a subversive. Ambition is only understood if it's to rise to the top of some imaginary ladder of success. Someone who takes an undemanding job because it affords him the time to pursue other interests and activities is considered a flake. A person who abandons a career in order to stay home and raise children is considered not to be living up to his potential - as if a job title and salary are the sole measure of human worth. You'll be told in a hundred ways, some subtle and some not, to keep climbing, and never be satisfied with where you are, who you are, and what you're doing. There are a million ways to sell yourself out, and I guarantee you'll hear about them.

To invent your own life's meaning is not easy, but it's still allowed, and I think you'll be happier for the trouble.


  — Bill Watterson, 1990 speech at Kenyon College

Someone, once again, is complaining about those misbehaving youngsters who don't understand the value of hard work. He lampoons the advice to follow your passion, saying that young people are just scared of hard work. He makes the dangerously misinformed claim that burn-out is just a myth: "Burn out is just a rationalization for giving up early."

For someone who seems so sure of what they're talking about, it would be difficult for him to be more wrong. Following your passion doesn't mean you do less work. It doesn't even mean you'll avoid doing boring things. A standard 9-5 job doesn't require you to think. You drive to work, get told what to do by some guy in a suit for 8 hours, then go home. It's about following instructions, not actually doing anything difficult. Most programmers are lucky, and have plenty of employers who give them interesting problems to work on. In fact, for calling his post "The Hacker News Generation", he doesn't seem to understand his audience very well. One of the current talking points is startups overworking their employees by expecting 80 hour work weeks, and how employees are trained to think this is ok.

That kind of seems like the exact opposite of an aversion to hard work.

Following your passion is immeasurably more difficult than climbing a corporate ladder. You usually work more hours for less pay, and often have to struggle to make ends meet. You have to do every part of your job, including all the mind-numbingly boring stuff, because there isn't anyone else to do it. People who are following their passion enjoy it more because they're doing something that's important to them, not because they're doing less work. An artist is the one drawing constantly, every day, barely making enough money to feed themselves. The guy at Microsoft writes a bunch of test code, checks it in, then gets lunch at the cafeteria. Where does this glorification of doing boring, repetitive tasks come from?

These people are busy climbing a ladder that society has put in front of them. They look out and see someone running around on the ground, away from the ladder, and become convinced they are hopelessly lost. Clearly, they don't understand the value of ladder climbing. When they realize that other people don't care about the ladder, they immediately conclude that these people don't understand what's important, because the only success they know of is the success they were promised by society at the top of the ladder.

A human being's worth cannot be measured in dollars or promotions. To follow one's dreams is not an act of cowardice, but rather one of incredible courage. To resist taking the easy way out, to avoid the routine of a 9-5 job, is to accept a life that is often filled with failure and hardship. The difference is that it will be a life worth living.

September 18, 2013

The Microsoft Word Problem

I use FL Studio 11 to make my music. Some people find this surprising, due to FL Studio's stigma as being a "toy" DAW that it simply cannot seem to shake, despite the fact it actually does most things as well or better than other professional DAWs. FL Studio has two real, significant issues: Large projects are unstable, and it has extremely bad 64-bit support.

This is almost never what people actually complain about. Instead, people complain about FL studio lacking features when they really just need to enable the right option. My friend lamented FL Studio's poor handling of automation clips without knowing about the "browse parameters" feature that lists all of a synth's parameters and highlights the one you are changing. He then complained that it didn't play notes when you started playback in the middle of them - this can be toggled in the audio settings. He also didn't know how to group multiple instruments to be controlled by the same piano roll, so I had to show him how to use Layers. Every single reason he gave for FL Studio being crap was just him not understanding how to use it properly, or not knowing where to find a specific feature.

This is the exact same problem Microsoft had with Word, and it's what resulted in the Ribbon GUI overhaul. Every reason people gave for not liking Word was just a result of them not being able to find the feature they wanted in an endless cascade of menus. To address this, it required a GUI overhaul so those features were grouped in intuitive ways, so people could find them more easily.

Even APIs can suffer from the Microsoft Word Problem. OpenGL, especially it's early incarnations, was essentially a giant, opaque list of functions that randomly referenced each other and had no clear organization. The online documentation is literally just an alphabetized list of every single OpenGL function in existence. The end result is that you need to know precisely what you want to do in order to find the right function. This is in contrast to DirectX, which (for example) has every single renderstate in a single, enormous enumerator. That enumerator contains something like 50 or so equivalent OpenGL function calls, which are not grouped in any meaningful way. It's documentation, like everything else on MSDN, is organized in hierarchical groups. DirectX tells you every single possible thing you theoretically might be able to do with your graphics card. OpenGL, on the other hand, has extensions, which are both a blessing and a curse. It makes OpenGL inherently more adaptable than DirectX, but at the cost of having absolutely no idea what the graphics card may or may not be capable of, because it's just not in OpenGL, it's in some extension you have to dig up.

Just like Microsoft Word, the features are all there in OpenGL, they're just harder to get to. And you'd better hope you know exactly what your technique is called, or you'll never find the function you want. WebGL inherits this problem, and throws on a few of its own inherent with attempting to bolt a low-level C api on to a high level web language that needs to be sandboxed.

We can't simply implement a feature, we have to make it easy to find, and easy to use, or it's worthless. It's amazing how quickly programmers forget the importance of context. In all of these cases, the solution to feature overload is conceptually very similar - Word created the Ribbon, which grouped related features under tabs and sub-groups. A similar grouping of OpenGL functions and parameters would do wonders for it's usability. FL Studio would also benefit by dumping it's left-hand browsing bar in favor of a Ribbon or module approach that didn't force 15 wildly different GUI styles into a single menu.

Context is important. If I want to find the function that sets the alpha blending operation in OpenGL, I should not have to go through over 300 functions to find it. I should be able to go to the "sampling" functions and look in the "blending" subgroup, and poof, all the functions that have anything to do with blending are there. It doesn't matter if we're designing word processors, digital audio workstations, low level APIs, middleware, or web applications. Anything and everything is susceptible to the Microsoft Word Problem.