Thursday, May 7, 2020

3D Conic Sections

You may or may not know what Conic Sections are.  They are a somewhat advanced topic in mathematics which describe a whole bunch of algebraic equations and relationships derived from a geometrical model. Basically, you have two cones of the same size placed on top of each other and then slice through them with any plane.

The downside is that it's almost invariably very hard to visualize what is going on.  In general, I recommend (conical, obviously) party hats at a sandy beach, but I thought that it would be a good thing to use to dig my teeth into WebGL before going onto the things that I more want to actually achieve.

Consider this picture from the referenced Wikipedia article:
It's not wonderfully clear what's going on.  But imagine if you will that instead of the cones staying still the plane stayed still as the surface of the screen.  You could then rotate, move and adjust the  cones and see the effects of your actions on the conic sections.  Moreover, by cunning use of the camera "near and far" options, I'm hoping that we can show just the resultant curve.

The Plan

This blog is meant to be about experimentation - but I'll be honest.  Often by the time I write things up here the experiments are complete and I know what worked and what didn't.  But on this occasion I'm really going in blind.  So let's have a plan:
  • Let's create a basic HTML outline with the two canvases and a simple model (of two cones) standing vertically as in the picture above.  I'll try to set the camera up correctly on the right hand one so that it just shows a pair of crossed lines (the degenerate case).
  • Then let's add a simple drag event as with the moon that enables the cones to be rotated while continuing to have their origin on the plane (which will continue to be a degenerate case).
  • Then let's add scroll or zoom events near the origin which enable us to move it back into the screen or forward out of the screen.
  • Allow full control by allowing a scroll or zoom event away from the origin to change the apex angle of the cones.
So I'm just going to wander off and create a project and some code.  I'll be back in a little while with a tag for you to check out to follow along; I'm going to quote some parts of the code that I find interesting, but if you want to see the whole thing you'll need to check out the repo.

Create a basic outline

I suspected that wasn't as easy as it looked like it might be.  You can check out the "current" state of affairs by checking out the tag PHILO_CONIC_BASIC_OUTLINE.

The first casualty was the thought that we could use two canvases.  I was thinking that we could just call the PhiloGL constructor twice (once for each canvas) and everything would be fine.  It turns out that when PhiloGL describes the "application" as an application, it means just that: it is the application covering the entire Javascript context.  I don't know (and didn't investigate) whether this limitation is due to PhiloGL, the underlying model or the underlying hardware.  Neither did I consider mitigation strategies - such as using an iframe.  Instead, I just chose a different strategy.  When the time comes we will try to place a semi-opaque dark square across the entirety of the z = 0 plane which I think should do enough of a trick.

Boilerplate

So the first thing that I did was to set up the outermost boilerplate: the HTML file and the same structure of a Javascript file with a nested PhiloGL application constructor call.  Within that, I have a fairly generic camera definition, a simple texture definition (which I will return to later), an onError method and an onLoad method which creates and renders a scene consisting of two cones.

Creating a Cone

Because I'm creating two cones, I decided to place the construction in a function:
function cone(yd, rot) {
  var ret = new PhiloGL.O3D.Cone({
    radius: 20,
    height: 50,
    nradial: 20,
    textures: ['img/balloons.png']
  });
  ret.position.y = yd;
  ret.rotation.set(0, 0, rot);
  ret.update();
  return ret;
}
I know what you're thinking: that looks complicated just to create a cone.  Bear with me.

The actual cone creation is quite simple.  It would seem that all the arguments are in fact options but you probably want to specify the radius and the height - the ratio of these two gives you the "slope" of the cone.

Because all shapes in WebGL are in fact just compound triangles, I found that the default cone rendering (with nradial set to 10) looked a bit "sharp" rather than rounded and found (by experiment) that 20 was the sweet spot (obviously the more you have the slower the render).

I had a lot of issues with the texture and I still don't really understand them; I'll come back to that in a bit.

Now, as I've said, I want two cones meeting at the origin: one facing up and one facing down.  When I first rendered the first cone, I was surprised to see that it was centered in the screen rather than (for want of a better word) sitting on the x-axis.

It would seem that the designers decided that the right thing to do would be to have the center of the cone be halfway up the central axis.  I'm not quite sure what drove this choice - in fact, I couldn't even confirm that it was true - but it means that we have to move both cones and invert the top one.

These "instructions" are passed in to the method as arguments.  yd is the "y displacement", i.e. the amount we want to move the cone up or down the y-axis.  The rot is a rotation (in radians) we want to apply in the XY plane (i.e. about the z-axis).

Every O3D model (including Cone here) exposes both position and rotation as properties you can adjust.  However, adjusting them is not, of itself, enough: you then need to tell the object that you have adjusted the properties by calling the update method.

Since we are only interested (at the moment) in moving the cone up and down the y-axis, we only adjust the position.y value.

The specification of the rotation boggles my mind, but basically it consists of three values: the amount to rotate about each axis.  In this case, we want a fairly simple rotation: 180º (π radians) about the z-axis which is easy enough to specify, but I feel that there must be some more consistent way of specifying this which I haven't seen yet.  Hopefully we will get back to it later in the exercise.

Building the Scene

With the ability to create cones in place, we can now construct the scene:
var top = cone(25, Math.PI);
var bottom = cone(-25, 0);

app.scene.add(top);
app.scene.add(bottom);

draw();
This is very simple: we create the two cones, add them to the scene and then draw the scene.

Drawing the Scene

The guts of the draw() method is probably where I'm weakest in terms of understanding what I did.  I mainly just copied the code that we looked at in the example.
gl.viewport(0, 0, app.canvas.width, app.canvas.height);
gl.clearColor(0.7, 0.7, 0.7, 1);
gl.clearDepth(1);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

app.scene.render();
As with the example, I have created a variable gl which is just an alias for app.gl.

The first line defines where the output is going to go and I have stuck with the assumption that we want to use the entire canvas.  The second line specifies the clear (or background) color.  I have chosen a "light" gray, but to the human eye it is more of a neutral gray.  Skipping over the lines I really don't understand, the penultimate line actually clears the canvas and then the final line renders the entire scene to the canvas.

Texture

Finally, let's talk about the texture.  I'd said before that I usually recommend (conical) party hats as the way to go in understanding conic sections, so I thought I'd carry on that theme by putting some balloons on the cones.

I kept - and keep - on getting an error about "there not being any texture bound".  Googling suggests this has something to do with the texture not being loaded, which makes sense, except it happens so much that it doesn't seem entirely plausible.  Anyway, I have just stopped paying any attention to it.

The basic setup is to declare a texture in the textures argument of the constructor.  There appear to be a lot of options you can specify, but I didn't understand any of them and nothing I played with seemed to make my life any better or worse.  So I ended up with the simplest thing that could possibly work:
textures: {
  src: ['img/balloons.png']
},
The image I downloaded off the internet was a JPEG, but it would seem that JPEGs are not supported.  This is based off the fact that I received a bizarre error message when I tried it.  I converted it to a PNG only that didn't work either; this time I received an error message about powers of two.  I then shrank my image to exactly 512x512 and saved that as PNG and that works.  I don't know the details.

It would seem that the src parameter becomes the id of the texture.  Anyway, as noted above, when creating the cone, we simply specify the value of the texture's src string as the texture we want to use:
var ret = new PhiloGL.O3D.Cone({
  ...
  textures: ['img/balloons.png']
});

Summary

Having understood the basic mechanics of building WebGL applications, I made a plan to build a conic section visualizer and started the process by building and rendering a simple scene consisting of two party hats.

No comments:

Post a Comment