12 KiB
Letter Rendering and Animation using WebGL
render some letters, animate it
Live Demo: https://demo.taufan.dev/webgl-letters/
Source Code: https://git.taufan.dev/cg/webgl-letters/
Note
Source code will be made public no earlier than Mon, 2024-09-22
What I came up with
I figured that the requirements itself are pretty lax; as long as the three letters are shown, one pair is made out of lines while the other is made out of triangle, and the background color changes while they cycle, I am technically in the safe side. I infer one thing from this: since there is no restriction to add more things, I should be good to do so.
That brings us to:
- Handmade animation timeline API
- Animated character and color transition using linear interpolation
- Custom (naive) parser to convert OBJ data into OpenGL VBO
Let me explain how I came up with each of these functionalities
The Absolute Minimum
The requirement dictates that total of 6 characters are needed to be made: 3 using lines and 3 using triangles. I handwrote the vertices required to form the lines (I did an attempt to make an SVG parser to generate this, more on that later). Also, since there is no statement in the requirement forbidding against computer-generated vertices, I decided to create an OBJ parser to do it for me (also, more on that later).
My base color is Madrasah Green (#3A5A40) and I got the complement
color from Adobe Color. To convert all the
HEX colors to normalized RGB, I used an
online converter
to do that (sorry Pak Onggo 🙏).
Shape and color is done, I think that cover most of our needs.
Vertex, Buffer, and Shader Organization using VAO
I found out about this cool feature called Vertex Array Object from a YouTube tutorial and this website. I highly recommend checking them out, these are invaluable resources!
Basically, VAOs bind together collection of attribute state to
reduce code duplications. These VAOs can then be bind to the WebGL
global state before doing a draw call. This is especially important
if you want to treat each character as its own object as opposed
to collection of 3 character. Keep in mind, though, that VAO
is natively available in WebGL v2. You can technically use VAO in
WebGL v1 through extension, but I just decided to use v2 as
it is more convenient, and baseline is widely available anyway.
Another good thing about WebGL v2 is that it allows me to use
GLSL v3.00 ES, which in my opinion has more beautiful syntax
(particularly in and out).
Other than VAOs, the basic functionalities like buffer loading, shader parsing, and program linking looks largely the same as the WebGL code from the other day. I split some functions into different file to make it a little neater, and got rid of the program object as it has been handled by my VAO generator.
The Shape Object
Initially, my idea were to create a base Shape2D class, that would then be inherited by each shape type (e.g. letter K, letter E, and so on). Each of these implementations would then contain their own set of vertices, rendering logic, position update logic, and so on. Later on I decided that this design would probably make sense if I were to instantiate each shape multiple times, which clearly is not the case here in this assignment.
Thus, I settled with the current design, creating a generic Shape2D class that holds together position vector, color, size, and very minimal stuff to do with WebGL rendering.
The decision on the shape class design was largely influenced by Daniel Shiffman, the author of the book Nature of Code.
Shaders
You may also notice that the shape object holds position and scale information. This is possible because later on, the vertex shader will do the transformation, by using scale as the dilation factor and its position as the offset. This new position is then normalized into -1.0 to 1.0 range based on the provided canvas size (provided via uniform).
The fragment shader doesn't do much, it just asks the programmer for what the shape color should be via uniform and apply the said value as the fragment color output.
I learned these techniques from the same YouTube tutorial and website.
Animation
I wanted to create a timeline-based animation API like they
have in Motion. Of course, I can't
really steal their code as it was created on top of native
Web Animation API, so I had to build it myself. I've worked
with animations before, my personal site
being one of them, so I knew right away that I have to at least
get the interpolation right. I was thinking of doing generalized
(t) => t' transition function (inspired by Svelte) but in the
end, I just settled with lerp as it is dead simple to make.
The first iteration was focused on making things work, and I was quite satisfied with the result actually. There are some problems, like the loop doesn't work and the shape that overshoots its intended final position because I forgot to clamp the interpolated value. The overall code itself was a mess, too. I think the Python programmers usually judges a certain code by how "pythonic" it is, and if there is such thing in Typescript, my code wasn't really near "Typescript-ic", if that make any sense.
I use Claude 3.5 Sonnet to tidy up my code and fix that clamping issue. Here are the prompts I used:
I am trying to create some sort of generalized animation timeline
for my WebGL project. Here's what I have so far:
<pasted my code>
The problem is, once a shape has reached its final position,
it will go past through its intended final position when
there's still more time
and
Is there any advice on how to make the code cleaner, and is
there any redundant part of the code?
The result turned out quite nice.
SVG Parser
Now, creating lines isn't that hard, and I guess the same could be said for the triangles, but I am sure you are likely agree that creating them are pretty tedious, particularly if you want to make them pretty.
I came up with an idea: instead of wasting 15 minutes to hand write the triangle vertices yourself, why not waste more than 10 times the amount of time needed to research and create some way to automate the process? Sure, it's weekend anyway.
My thought was to attempt to generate VBOs from a well known format, so I can create the text itself using some sort of software, to then be parsed. My first instinct was to make an SVG parser, since it is human readable and I have worked with them before. I did some research on this from:
- Reddit - How do I convert SVG image into an array of vertex
- Gamedev StackExchange - How can I generate vertex data from SVG
- Google Group - SVG path to vertices/indices array
- Processing - SVG to vertex code for P5JS
Eventually, I tried to play around and even asked Claude (LLM) to create the base implementation. The general idea is, because SVG path is like a pen, we just need to convert the end position into a vertex data and normalize it at the end of parsing.
This works somewhat okay for lines, but since SVGs itself aren't typically triangulated during the creation process (except probably you explicitly make it to triangulate itself using some fancy softwares), you'd need to triangulate those vertices yourself. Now, triangulation isn't exactly easy and it could be a topic on its own, so for now I decided to ditch the idea completely and think of something else, which brings us to OBJ.
Wavefront Parser
Now, you might think that creating an wavefront parser is unreasonable given that my attempt to create an SVG parser fails, but let's recall that:
- Wavefront is just as human readable, if not more human readable than SVG
- Because it is designed to work with graphics API, there must be quite a lot of people doing their implementation of parser
- 3D softwares like Blender typically has built in triangulation function, so I do not have to do it myself
- While what we're aiming for is 2D VBO, we can achieve that just by ignoring the third axis from the generated wavefront file
After reading its entry on Wikipedia, I got a general sense on how the format works. My first attempt on making this parser was to grab all XZ vertex from the file (that has been conveniently normalized) and take it as the VBO. Of course, since it only contains vertices, they still lack information required to actually be rendered as triangles. Though, if I were to render it using line loops, the shape turned out to be quite good.
I've consulted to some resources, like this website and Claude Sonnet on how do I go about creating a wavefront parser, and they lead me to creating separate buffer for vertex array and indices array. Now, this is a valid approach and even more optimized, this is probably not that hard to implement considering I have set up VAOs, but I just want it simple and take the faces information to push for vertices to the final vertex buffer, so I did just that.
Is it optimized? No. Does it work? Absolutely.
Just in case you want to use it for yourself, navigate to the
/scripts directory and use the provided python script.
cat ./in/yourfile.obj | python ./objParser.py > ./out/yourfile.out
Acknowledgements
These resources helped me a lot in doing this assignment:
- Nature of Code (book) by Daniel Shiffman
- webgl2fundamentals.org
- Indigo Code (YouTube)
- Gamedev StackExchange
- OpenGL Subreddit
AI Involvements
I try to avoid as much AI as possible so I can fail and learn, but there are some instances where I use LLMs to help myself save some time:
- Refactoring the animation API
- Base implementation of SVG parser (unused)
- Base implementation of Wavefront parser
All these are done using Claude Sonnet 3.5 by Anthropic
More Resources
While not directly related to this assignment in particular, these people have helped me in the past to learn about computer graphics, so check them out!
- Yan Chernikov - ex. EA, now creating Hazel game engine
- Daniel Shiffman - board member of Processing Foundation, prof. at NYU, founder of The Coding Train
- Grant Sanderson - founder of 3b1b, has created tons of video explaining various topics in math
- Sebastian Lague - created tons of video explaining topics in CS and math

