TheSeamau5 / GraphicsEngine

3D Graphics Engine in Elm
16 stars 5 forks source link

Add sphere mesh generator #1

Closed mgold closed 9 years ago

mgold commented 9 years ago

Add a function to create a mesh that approximates a sphere. The code is ported (pretty directly, surprisingly enough) from some C++ I wrote in a college graphics course. We approximate a sphere by a parametric number of segments radially and vertically, with each r,y segment pair becoming a quad. I have some more primitives from that college course which I can discuss if you like the sphere.

Some other things I noticed working with the library:

TheSeamau5 commented 9 years ago

Wow! Thanks for your enthusiasm. I have an implementation of a sphereMesh in my codebase which sorta works the same way as your (a bunch of latitude and longitude lines connected as quads). Although, since I just thought of the implementation, my code looks much messier than yours. So, as soon as I try your code, verify that everything is good, I'll merge the pull request.

I have a basic implementation of lighting but it's still not there yet. I'll switch the fragment shader to a rainbow colored shader so you can tell the perspective, because, it's supposed to be a perspective camera. Farther objects look smaller. It's just harder to see the 3d-ness with just the red shader.

As for normals. So, when I started the project, I knew next to nothing about 3d graphics. I just know math. So, I thought, "hey, let's compute the normals in the GPU." That would mean that I'd only send a position attribute and that's it, the rest would be calculated in the GPU (hence the little GLSL library I made). That worked....until I started messing with lighting...and tried placing a fair number of objects on the scene...and I realized that this is an expensive process. So, rest assured, the normals will be computed on the CPU and passed in as an attribute. This means that the attribute type would look like this:

type alias Attribute = {
  position : Vec3,
  normal : Vec3
}

So, for all the record types, yeah, I could make a flowchart. You're right that the code is very much split apart. I would've loved for more affordances in the way you export libraries. It kinda puts me in a pickle. The short version of the problem: code is too long to be in one file (or else my brain explodes), and it is too short to be scattered everywere and I currently can't think of a smaller folder structure.

I like to divide up my code into the smallest possible units. It allows me to stay sane as more functionality is added. Maybe it's an old OOP habit, but it's sorta how I think. But I am open to suggestions.

But I don't want to slam everything into the Engine file.

I want to give the minimum necessary to the user to draw scenes through a single export. That said, I still wanna give the user access to everything in the codebase, so everything gets exported.

There is also this weird thing with the docs is that when I alias a type to re-export it, the docs don't show the underlying structure. It just shows that

type alias Renderable = Renderable.Renderable

which is really not helpful. So, I have to leave the link to the renderable file so the user can look at it.

Different sections of the code do have strong dependencies on each other and are not totally indivisible and some merging could help. Currently, all the magic happens in Engine/Render/render.elm. Those few lines of code start everything. Currently, a lot of the code in the codebase is just boilerplate. Not very interesting. The shader stuff takes up space. I made it modular so that I can just easily add new uniforms and new attributes and stuff. I could've easily written a large multiline string and threw in everything, but I think there's value to keeping it programmatic.

Now, for the documentation, well, in lieu of examples and tutorials (because I don't have time), I wrote in loads of documentation. The codebase did go through a couple of major changes so there may be a few oversights here and there but I'll make sure to correct them. (If you spot anything weird, just open up an issue.)

As for the .gitignore, I was just so happy that I got the whole elm-package publish thing to work that I completely forgot to clean up the extraneous bits. I'll do so. It's not very important but it's for good measure and it leads to confusion, cuz these files have nothing to do on Github.

Finally, yeah, I've noticed weird behavior with rectangleMesh when trying to make a sphere. I do not know why. I tried to modify the triangleMesh and rectangleMesh code in all sorts of ways but I just got more and more weird things. I read somewhere that the front facing points should be enumerated in clockwise fashion and the back facing points should be enumerated in counterclockwise fashion. I don't know, my sphere seems to work now. Why? I don't know....

Thanks for you help and your enthusiasm. Your remarks and advice are very helpful thanks. I'm still just in the learning process with functional programming and I find Elm to be really good at that. So, please, I'm sorry if there is a lot of newbie-ness that would seem to emanate from this codebase. It is my first non-trivial codebase in the functional style and in Elm.

I hope that I've answered a few of your questions. I am interested in more primitives :D. The more the merrier. I kinda envy the list of primitives that THREE.js has but unfortunately I don't know how long it'll take me to get around to adding them all.

Feel free to contribute as much as you want and to do whatever you want with the code. My hopes with this project is that:

1) To test this weird API design of just declaring your scene as data and calling render. 2) To give me the necessary affordances to treat 3d graphics with the same ease as 2d graphics (for future and concurrent projects). 3) To contribute to the Elm ecosystem and give back cuz everyone's been so helpful with me.

mgold commented 9 years ago

Thanks for the long response and intent to merge. (It's hard to debug with just a red circle, and I want to make some kind of navigator control eventually.) I'll try to get to everything.

You can compute normals for a triangle knowing only the three points, but from what I've seen, you have normals for vertices. So you can't compute them out of context. Hence, the primitive also declares the normal. Then you make a collection of primitives that you scale, rotate, and translate as you please. The library designer is responsible for providing many primitives so custom meshes are usually unnecessary. OTOH, it seems many meshes from other sources don't have normals, so this might take some more digging. Normals are essential to lighting.

If you're interested, the course website is still up (it was only last spring) though I can't guarantee for how much longer. Stop when they get to raytracing, that's a totally different ballgame. The instructor's code is pretty crazy, with structs whose fields are undefined unless an enum has the right value (hello union types!). It's so unportable that we demoed on our laptops instead of him compiling our code. This is part of why I'm interested to see graphics in Elm. The primitives I have are from assignment 1. They are

A group of primitives, with its own transformation, could be useful, but I have no idea how difficult the implementation would be.

Depth: I like the idea of a distance-dependent default shader, though you may want to reconsider the rainbow color scheme for something less visually abrasive. I can help with that, especially if you get rainbow working for me to change.

Modularity: I can try (locally) throwing things into a single file, e.g. Engine.Mesh, and see how bad it is. This might encourage more qualified imports. It might be useful to mock up breaking changes in another branch, but for now I can go with the existing architecture. Maybe Engine.elm can be generated from the docs json?

I've heard of three.js but never used it much. We can mirror their object structure where it makes sense, and crib from their primitive generating code. It's also a good checklist of what a graphics engine needs.

The handedness of the triangle determines whether they are front or back, and webGL ignores the backwards ones. That was my first suspicion but I changed the order around and it still rendered with triangleMesh so I'm not sure.

I like the API decision of rendering data, since that data is easy to change with signals. You'd be surprised how difficult motion is in C++.

It might be a good idea for you to come up with roadmap to state and prioritize what you want. I'm happy to give it a shot.

TheSeamau5 commented 9 years ago

So, for modularity, it would be a nice experiment to slam everything into one file. It would be nice to try it and compare the two.

I could possibly open up a new page and put everything there and then, when ready, merge with the master branch.

By the way, your course looked like a fun course. The projects all seem interesting (if you don't mind me checking them out that is).

Normals:

The idea is to partly compute normals beforehand. Your mesh (cube, sphere, etc...) would have some basic normal values. And then you'd do the trick of computing the transpose of the inverse of the model matrix, which gives you the normal matrix. This gets passed on as a uniform and then,

vNormal = NormalMatrix * normal;

Camera:

I think in the medium term, we'd want two cameras: one orthographic and one perspective. I think that this suits 99% of all use cases. One can imagine adding a fisheye lens-like camera but that sounds so rare and precise that it's better if the user makes that theirselves.

API:

As I was playing around with Elm, I realized that a lot could be done with records and immutable data. Then I got this thought: What if you could just easily JSON serialize a scene and just call render on that data? Obviously that's partly what Maya and AutoCAD and the rest do. Except that I bet that the process must be buried under an endless list of classes and subclasses and headaches. This is more like, "you just write the data. you call render. you get a scene." The whole trick with the API was how to structure in such a way to please the type system because you can't have heterogeneous data structures and the types must be well-known at compile-time. So, there are trade-offs.

One can imagine a final form of the scene type to be something like

type alias Scene = {
  lights : List Light,
  objects : List Renderable,
  camera : Camera, -- or List Camera
  particles : List Particle,
  viewport : Viewport -- or change to sceneOptions, in case we need some flags here
}

I'm considering also moving from List to Array. Unfortunately, there is no special syntax for Array. It sounds like a nitpick but the goal is to abstract away as many details as possible and having to call Array.fromList has nothing to do with the actual intent of the code.

I'm a bit minimalist in that sense, I hate extraneous code. This is why I grew tired of C++. Worrying about things like "the rule of 3" or freeing pointers has nothing to do with the problem I have. So, yeah, I'm well aware of how gnarly C++ can get and now that I've discovered that there is most of the stuff I had to do in C++ could be taken care of by the compiler or runtime, I've lost patience with it.

Roadmap:

The goal of this library is to have a simple library to create 3D scenes. It is not intended to be full-featured or fully flexible but it should give the affordances necessary to do 90% of the things you might need. This should include:

What this list does not include:

To do this, it is necessary to remove all the unnecessary boilerplate that goes against understanding how 3D graphics works. Sure, buffers and stuff are important to understanding the details of 3D graphics but, at first, it would be better not to see any of it. The JS WebGL API is a very old-school C-style API and libraries like THREEJS, as awesome as they are, still have a very imperative approach to them.

When learning about 3D graphics, I've been put off by all the boilerplate you have to write in OpenGL/WebGL to just display a triangle. We're in 2015, we should by now just be able to say draw triangle and that draws a triangle. Even THREEJS requires some set up to get it to do it.

This level of simplicity is what I'm looking for in this library above all else. There is already a wealth of 3D libraries out there that help create high performance 3D applications. This is not what I'm aiming for. I'm aiming for the simplest 3D library possible. Sure, it won't be the flexible or extensible library out there, but it would be easy to use.

The idea of using a "data-oriented" API would be that JSON serialization becomes trivial, so rendering a scene from a JSON string would also be trivial. Imagine the following function:

renderFromJSONString : String -> Maybe Scene

The other big gain is that you can then represent an animation as a list of "scenes" (which become frames) and each "scene" would also have a timestamp

So, you can imagine:

type Frame = Frame Int Scene

type alias Animation = List Frame

animate : Animation -> Signal Element

From these types it should be obvious that playing a scene in reverse is trivial:

animateReverse = animate << reverse

You can't get more declarative than this, to be honest. There's not one extraneous character or statement in all of it.

Personal goal :

My personal goal is to learn more about Functional Programming. This project fits into my general goal to immerse myself into FP because I feel like I've only known Object Oriented Programming and, at first, I wished to expand my horizons. Now, I know what FP can do and I love it!

Furthermore, I think that a project such as Elm should be pushed, even if only to inspire the programming world to move more towards FP. I think that projects such as this can help push Elm to see how far it can go as a platform. But that's not a real goal, more like a wish.

Originally, my idea was to make a Civilization-like game in Elm. So, I started making it and at one point I was like "you know what would be cool, if the map was a sphere as opposed to being flat". Then I realized that doing anything in 3d was way harder than just calling a function like one of the Graphics.Collage functions. So, I set out to correct that.

mgold commented 9 years ago

I can try writing a ruby program that reads the docs json and spits out Engine.elm. By all means, check out the final projects. Orthogonal and perspective cameras sound fine. I'm going to have to do my research on normals. You have read through Animate Your Way to Glory, yes?

One of the things that frustrated me was how difficult motion was in OpenGL, which partially inspired my final project. I like the three.js trackball but the only documentation is a full example, which I couldn't get working with another piece of code I had. It's completely uncomposable. I'm working toward something like trackball : TrackballOptions -> Signal Camera where TrackballOptions includes things like sensitivity and inertia.

My long-term goal is to take this 2D visualization and make a 3D version, as outlined in this note. I share your exasperation that it's not simple to do.

So yeah, I'm very much on board with the roadmap. Stunningly easy for simple use cases, with animation/motion/interactivity/dynamism made easy by signals. I think this is where having the scene be transparent data will be a good thing. So while having a list of frames up front may be one way, you can also generate frames in response to randomness or interaction, and maybe save them for reversible playback.

So, visualizing 3D geometry should not be any harder than describing 3D geometry.

You can't get more declarative than this

I like to point out that imperative is the opposite of both declarative and functional.

Let me know when you get a chance to verify and I'll do more primitives, and maybe that ruby script. Again, it's easy enough to add normals for the primitives themselves; the problem is if people bring their own meshes or surface maps or what have you.