This picture shows the final result of a 2-semester long project to construct an underwater scene in C/OpenGL. It took a lot of work to get to that point as almost everything had to be written from scratch. There are no fancy libraries or game engines, just pure pain. Before I get too far into this, this is not some highly original concept. I was inspired to recreate this scene from the video below. I’ll also be borrowing some ideas to make this all work.
It’s hard to figure out how to get going on a project like this. What should come first, the fish or the terrain? I decided on starting with Boids and pushing the nightmare that is terrain generation onto the backlog.
Boid’s algorithm isn’t really a tough egg to crack. For those that are unfamiliar, it’s just 3 simple rules:
Put visually:
See Boids - Background and Update, Craig Reynolds for more details.
That’s really it, for now. We’ll come back to this later.
Okay, I may have lied; this part was sheer pain. Anyway, let us get into it. Inspired by the video linked above, I wanted to generate a world with the Marching Cubes algorithm - Marching Tetrahedra struck me as a bit intimidating.
The marching cubes algorithm explains how to generate a set of triangles to approximate a scalar vector field. More simply, if you have points in a 3d space, this is how you generate triangles to approximate a ‘surface’ that those points would create. Don’t worry too much if my 2-sentence crash course didn’t do the trick, there’s going to be some helpful graphics below.
Marching cubes is not something that will generate a world in isolation. There has to be some sort of input. Marching cubes take in a function and outputs a set of triangles. So what should that function be? Well, the good news is that I don’t have to solve this problem. Smarter people have come along and tackled this a long time ago. Perlin noise is a multidimensional function which when paired with marching cubes produces life-like terrain. Again, we’ll visualize this process below, keep going!
It should be noted that I’ll use Simplex & Perlin Noize interchangeably here. For all intents and purposes, they’re the same thing, Simplex is much faster.
Here’s the 4-step plan to get the world generation working:
In hindsight, it was not that easy.
The above shows my first crack at my 4-step plan. There are also a few boids in there - because why not. It may be apparent that this is pretty terrible. Firstly, we need some colors. This could be done in so many different ways, the approach I went with is using the noise function to generate a lower frequency function mapped to a color. So when I assign a noise value to a point, I also generate a color value.
Next, the size of the world has to be a lot larger. I had a small world initially because the program was terribly unoptimized. The problem is that I have an awesome GPU capable of handling these complex calculations in parallel but instead chose to do each calculation sequentially on the CPU. For prototyping this was fine, but it was time to get into the nasty stuff.
Before, all the work for drawing the world was being done on the CPU. This is only okay for small amounts of work. In my case, I wanted to recreate the entire world every frame so the CPU was not going to cut it. While a CPU has maybe 8 cores, a GPU has many, many, more cores. This means that parallel workloads can be completed much faster. Moving the relevant calculations over to the GPU is not a trivial task.
Firstly, a VBO is assigned. This is a block of memory reserved in the GPU for our calculations. Ideally, we will write to this memory space with our input data, kick off the calculations, and ask for the GPU to write the resultant triangles to the active frame buffer. I could spend a long time explaining my horror stories of this process, but it’s easier to show you instead:
How did I fix it? No idea. But my hard work did pay off. Now the world is a bit larger and it has colors!
So you may have noticed that the boids disappeared in the last video. They have not been forgotten about. A lot of work is needed to integrate them into the shader hellscape I have just created. Firstly, we need them to spawn around the player. Since the world is infinite, we cannot spawn a set amount of fish and call it a day. To accomplish this, I broke the world up into 9 chunks relative to the player. When a chunk is too far, it gets deleted. When a new one is created, it’s populated with a bunch of new boids.
When the boids move throughout the world, they better not phase through anything. If this were Unity, we’d throw on a box collider and call it a day. OpenGL gives you nothing of that sort. So collision detection and avoidance will have to be written entirely from scratch.
Firstly, if we’re a boid, how do we know if we’re about to hit something? If you said anything to do with sight, you’re completely wrong. As written, this program does not store the state of the world. It’s regenerated every frame inside the GPU. The game itself does not know about the definition of the world.
Instead, we have to perform a voxel-raycast outwards from the perspective of the boid, generate the world geometry from the voxel raycast, and perform ray-triangle intersections with the generated geometry and the boid’s velocity vector.
For those unfamiliar, a raycast is the process of shooting a ray and performing operations at different points along that ray. For this, I used A Fast Voxel Traversal Algorithm for Ray Tracing.
This is a quick visualization I made of a blue ray intersecting 4 red voxels (interactive):
So, we have the cells we need to test. Next, the cells are passed in as points to the world generation algorithm to create the world’s triangle geometry. Once we have the triangles, the ray has to be tested to see if it hits any of the triangles. If it does hit, we have to steer the boid away.
The ray-triangle intersection is a relatively easy problem to understand and solve if you do not want to go crazy on the optimization. When you throw away the need for performance, it’s just 3 steps:
Calculate the cross-products of the sides of the triangle to the point
What do we mean by this? Let us start with this diagram:
Now make your right hand into this:
If AB is a and AS is b, then your thumb points out of the triangle, the same direction as the triangle’s normal vector. But now imagine that the point AS is below the triangle. If you do the same thing you have to flip your hand around (no > 180 degrees allowed). That means that your thumb would oppose the normal vector.
The code is unbelievably simple:
# A,B,C are triangle points
# S is the ray origin
# D is the ray direction
def intersects(A, B, C, S, D, color='red'):
# N = The normal vector of the triangle
N = cross(A - B, B - C)
N /= norm(N)
# Force the normal to oppose the ray
if (dot(N, D) > 0): N = -N
# t defines the length of ray needed to intersect the plane
t = dot(N, A - S) / dot(N, D)
# dot(N, D) could be zero, member to do a <=zero check
# dot(N, D) == 0 means that the ray and plane are parallel
if (abs(dot(N, D)) > 0.001 and t > 0):
PlanePoint = S + D * t
PN = [
cross(B - A, PlanePoint - A),
cross(C - B, PlanePoint - B),
cross(A - C, PlanePoint - C)
]
PN = [pn / norm(pn) for pn in PN]
intersect = all([dot(pn, N) > 0 for pn in PN])
if (intersect): return PlanePoint
And here’s what it looks like in action (interactive):
The cross-product vectors are shown at the 3 edges of the triangle (red). The ray (light-blue) from the point source (orange) intersects the plane formed by the triangle at the point (green). The normal vector for the triangle is shown in dark blue.
Apply that to all the triangles generated from the previous voxels, and we’re in business.
Okay, we still have to make the fish avoid the obstacle. That is not at all trivial. I have a solution, but it isn’t pretty…
Initially, I thought I could use the normal vector to steer the boid away from the terrain. That was a giant fail. Since the geometry can be very complex, there are pockets that boids can get stuck in. I shamelessly stole Sebastian League’s solution. Here’s the rundown:
These rays are straightforward to generate. Here’s the code for generating a Fibonacci spiral:
num_pts = 100
for i in arange(0, num_pts):
index = i + 0.5
phi = arccos(1 - 2 * index / num_pts)
theta = pi * (1 + 5 ** 0.5) * index
x, y, z = cos(theta) * sin(phi), cos(phi), -sin(theta) * sin(phi);
Assume that the boid is facing upwards, here is the algorithm generating “alternate” paths:
As easy as that all was individually, it has to be glued together. This was in no way fun. But you all get to see more cool videos. Here’s one that I took showing the ray, intersected voxels, triangles, and alternate paths being generated. You might notice, those green triangles look a bit off. That’s right, I wrote all of this code in C to run on the CPU, but the world generates on the GPU. There is a floating-point precision difference between the CPU and GPU that I did not have fun resolving. Put shortly, everything had to be rewritten in GLSL shaders.
Once that was all figured out, it was pretty cool:
Firstly, the fish are not well… fish-like. Let me have a go at that.
The easiest way to go about this is to import a pre-made fish model as an obj. Obj files, seemingly like everything else in OpenGL, are not supported out of the box. It takes a lot of finessing to get the meshes to load properly into shaders. One issue that I spent way too long working on was that my model used a mixture of quadrangles and triangles for the mesh. Shaders will only accept triangles, so performing a triangulation on the mesh before importing is a big plus.
Applying a simple sign-wave translation to the vertices of the model adds a sort-of wiggle that emulates how a fish would swim:
If you move forward, you’ll see the world generating in bit-by-bit. It’s best to cover that up. This is probably the easiest part, just 3 equations:
\[\begin{align} F_{\text{linear}} &= 1 - \frac{\text{fog end}-d}{\text{fog end} - \text{fog start}} \\ F_{\text{exp}} &= {e^{-(d * \text{fog density})}} \\ F_{\text{exp2}} &= {e^{-(d * \text{fog density})^2}} \\ \text{Pixel Color} &= \text{mix}(\text{color}, \text{fog color}, F_{\text{linear}} + F_{\text{exp}} + F_{\text{exp2}}) \end{align}\]A caustic is a pattern of (often white) light resultant from refraction or reflection. In the ocean, this can be seen as a wavy pattern on the seafloor. These are incredibly difficult to generate in real-time over such a large scene. For that reason, I cheated. I used https://www.dualheights.se/caustics/ to generate a sequence of 64 tileable caustic images that I project directly downward onto the seafloor. To be frank, it’s dirty and only looks good from far away. I’m mixed, but I’ll keep it here for now.
Ambient Occlusion refers to how a point is exposed to ambient light in a scene. So if we have a point underground, it probably isn’t going to be very bright. You may have noticed that caves are awfully bright in my hyper-realistic scene, so a change might be warranted.
To calculate a lighting value for each point, I took the following from GPU Gems 3:
To compute an ambient occlusion value for a point in space, we cast out 32 rays. A constant Poisson distribution of points on the surface of a sphere works well for this. We store these points in a constant buffer. We can—and should—reuse the same set of rays over and over for each vertex for which we want ambient occlusion.
In human-speak, check your nearby surroundings to see if there’s a lot of nearby geometry. Adjust lighting proportional to the number of neighbors.
So after all of that, and a lot more is put together, we have a finished scene. This was one of my all-time most favorite and hated projects. I hope you all enjoyed it. Here’s a final demo:
]]>Pen plotters are extremely simple machines by nature. Most commonly, two axes are manipulated to move a print head to a desired location on a flat surface. Below offers a quick example:
Pretty neat?
One thing you may have noticed in the above video is that there’s no clear mechanism for moving the ‘print head’ around the coordinate system. It’s all contained internally within the segments of extrusion. This is a very clean look and very uncommon.
The mechanism in question is commonly referred to as ‘H-Bot.’ This is not to be confused with a more popular kinematics option called ‘CoreXY,’ which is a prevalent design choice among 3d printers.
The main principle between both of these options is that there is a continuous section of belt running throughout the extruded segments and terminates on each opposing side of the print head. Then, through clever manipulation of the motors, the print head can be maneuvered throughout the plane without directly mounting a motor along the y-axis.
The electronics is seemingly an already solved problem. Extremely cheap Arduino hats are sold for very cheap for this exact purpose.
These are slightly different from what might be used in a 3d printer in just a slightly agitating way. The main distinction is that these boards expect CNC-flavored G-Code.
Drawing is hopefully a simple task. The reference design at the top uses a solenoid to force a pen or marker into the work surface. The benefit of this is that no intricate mechanisms are needed on the Z-Axis. An on/off (drawing or not drawing) level of granularity is sufficient.
It pains me to say this, but I skipped documenting a lot of the design process for this project. In place of that, I do have a few highlights which I found interesting.
I originally designed the Z-Axis to have 4 grooved bearings riding within a small groove. This mechanism proved to be completely unstable and not fully constrain the motion of the pen. It’s easy to overlook the small forces at play but the multiplication of these forces by the lever of the pen dragging on the work surface made a redesign unavoidable.
My next attempt replaced the grooved tracks with 2 linear rails:
It’s never fun to have your electronics spilling out onto your work surface, so I designed a nice little enclosure to keep things tidy:
This phase went pretty smoothly since there isn’t a great number of complexities. I’ll use this section to share a few milestones.
This test is interesting. You’ll notice that the print head isn’t doing anything. That’s because the mount connecting the z-axis motion to the solenoid melted mid-print. This part was initially printed in PLA with is going to lose a great deal of its strength in any amount of heat. Reprinting this part in PETG raises the temperatures that can be withstood significantly.
Here’s the final product. To preempt the obvious, the camera work is substandard—hopefully I can reshoot this in the future.
I did not get the time to capture too many drawings made on this machine, but here is one of my favorites:
There’s still some room to improve (emphasized by the first travel move not properly retracting the print head) but it’s not that bad.
Well… that’s it. I don’t think this machine is going to win any awards, but it definitely tops my jury-rigged pen plotter made about 2 years earlier on my first 3d printer:
I’ll end with my favorite picture of the entire project. It’s near-impossible to do something perfectly on your first try. I certainly attempted to but fell well short of this ideal. A lot of the parts had to be re-printed and re-designed several times before they would even fit together. Below is an image of all the parts that didn’t make it into the machine.
]]>Lately I’ve noticed a lot of similar YouTube channels popping up in my feed. I could go at length trying to describe this format to you or just give you an example. I think the latter would be easiest:
That video specifically has 19k views at the time of writing this. And there’s more like it, just type “Reddit” into the YouTube search bar:
Every single one of these videos, literally all of them, are of almost the exact same format. It looks something like this:
None of those steps exercise any use of creativity. That makes it the perfect process to automate.
Here was my original plan:
This is straight forward. Reddit offers a public API.
All that has to be done is to call https://reddit.com/r/<subreddit>/top.json
and you get all the top threads for a
subreddit. My target for this project is /r/AskReddit.
Then once you have a threadId
just call https://reddit.com/comments/<thread id>/top.json
to get the top comments.
That’s it.
Scraping Reddit wasn’t so bad, no way images could be that hard… WRONG!!! I would estimate that around 50–70% of the time spent working on this project went towards image generation. I wouldn’t have thought it, but it appears to be uncommon to want to render HTML to pictures.
The first part of the generation is straightforward. The text of the comment has to be broken up into sentences and each sentence needs its own image. Then the images can be strung together to make something like this:
I had many ideas on how to go about this:
java.awt.Graphics
.As an exercise to the reader, try to guess which methods worked and which didn’t. I tried all of them and only one kind of worked.
How patient are you? Chances are, you do not have the patience required to implement this. Drawing such a simple image is a burden in awt. Even just drawing text that doesn’t look horrible requires the follow blob of code:
// Load the font
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
Font font = Font.createFont(Font.TRUETYPE_FONT, fontFile);
ge.registerFont(font);
// Turn on text anti-aliasing
graphics.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
graphics.setRenderingHint(
RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_ON);
// Set the font size
Font newFont = font.deriveFont(font.getStyle(), fontSize);
graphics.setFont(newFont);
// Draw the string at x,y
graphics.drawString(text, x, y +g.getFontMetrics().
getAscent());
And that was just to draw a single string. Yeah, AWT is tedious. I don’t have the patience to redo HTML in awt so on to the next idea.
This was actually the first method I tried because it looks straight forward. You extract the styled HTML from Reddit, add in some placeholders, and render it.
Extracting the HTML is made straightforward by some amazing tools:
The created snippet can be then exported to CodePen, jsFiddle or JS Bin with one click.
I did notice some issues with non-escaped unicode in CSS properties not being encoded properly. Instead
of content: "\uF100"
it would be content: '"□"'
. So after a bit of post-processing, the file is good to go.
So HTML extraction is complete. Then some placeholders are added into the HTML file. Then all that’s left is to render it.
Q: What could go wrong?
A: EVERYTHING
The only Java API I could find that claimed to render HTML was flyingsaucer. This library didn’t claim to work with CSS3, but I tried it anyway. The results where completely unusable.
But what about Chrome? I render hundreds of HTML pages a day with Chrome. How about I give that a try.
Chrome offers some appealing features in its CLI mode. It can be run in headless mode, meaning no visible browser window is opened, and it can screenshot a web-page. The only problem is that I don’t want the whole web-page, just the element I’m trying to render. So Chrome CLI is a no-go.
This is very similar to the approach at the end of method 2 except I’ve added on some more tooling. I probably could have gotten method 2 working, but I liked how this approach doesn’t require me to fix unicode literals every time Reddit does a redesign.
The idea here is as simple as it gets: Go to the comment URL. Empty the text of the comment. Add in a sentence. Screenshot the DOM element. Repeat.
To screenshot individual elements on the page, I used puppeteer. In their own words: “Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.”
So it can control Chrome. But most importantly, it can “generate screenshots and PDFs of [DOM elements on] pages.”
To run Puppeteer, a separate Node.js application is required :(. This just needs to take in the link to the element and take a picture of it.
The essence of this application is summed up in the following bits of code:
Taking a screenshot:
// Get a box around the element that will be screenshotted
const boundingBox: BoundingBox = await this.page.evaluate(selector => {
const element = document.querySelector(selector);
...
const {x, y, width, height} = element.parentElement.getBoundingClientRect();
}, parentSelector);
... add boundingBox to screenshotOptions
const b64string = await this.page.screenshot(screenshotOptions);
Taking the incremental screenshots:
const textHeader = await this.page.$(textSelector)
const textDiv = (await textHeader.$x(".."))[0]
// Split by sentence
const parts: string[]
= splitString(await this.getHtml(textDiv))
// Set text to ""
await this.page.evaluate(e => e.innerHTML = "", textDiv)
// Add in text part by part
for (const part of parts) {
await this.page.evaluate(
(e, part) => e.innerHTML = e.innerHTML + part, textDiv, part)
const base64String = await this.screenshotDOMElement(parentSelector)
}
And that works. It navigates to the page of the comment and takes the screenshots.
Text-to-speech is a problem that has been solved over and over by so many different people. The problem is that nobody made their solution that good. So many of the TTS engines available, especially the free ones, where unusable.
Mary, a free TTS engine written in Java, has very poor sound quality:
Vocalware (Most common in Reddit YouTube videos) Has exceptionally better audio quality. I also noticed that they billed me 5 separate times for $100 before my trial period had expired. No thank you.
The only problem is the cost: Streams require a $100 minimum payment.
Amazon Polly Offers TTS at the same/better quality as Vocalware but has a much better pricing model. ~$0.03 for a 10-minute audio transcription.
Amazon has already written an amazing API for Java. So implementing the TTS is basically just this:
SynthesizeSpeechRequest synthReq = new SynthesizeSpeechRequest()
.withText(ssml)
.withVoiceId(voice.getId())
.withTextType("ssml")
.withOutputFormat(OutputFormat.Mp3);
SynthesizeSpeechResult synthRes = polly.synthesizeSpeech(synthReq);
return synthRes.
getAudioStream();
I wasn’t able to find a suitable API to render videos in Java, so I had to write my own. This was way more fun than I expected it to be.
If you want to render a video and not pay large sums of cash, the go-to option is FFmpeg. There are enough options loaded into FFmpeg that you could create your own Sony Vegas from scratch (I kinda did). There are so many options available. The documentation page has 159,949 total words (https://wordcounter.net/website-word-count). For reference: Harry Potter and the Sorcerer’s Stone by J. K. Rowling is 309 pages and has 79,185 total words.
The other option is to use a frame server such as AviSynth. This offers a cool scripting language to edit videos. I learned about this too late into the project to fully explore what’s possible with it.
So FFmpeg it is. Onto the abstractions.
I wanted the editing API to be independent of the rendering API that way if FFmpeg goes out of fashion, I can swap it out for something else with relative ease. I wanted something like Sony Vegas or its clearly superior Windows Movie Maker. The most important aspect is being able to load in a file, move it onto the timeline, reposition the image in the scene, and add image filters like a stretch or shrink.
In short:
Map<Timestamp, TimelineElement>
.TimelineElement
is a filterable instance of a Media
So now there exists a timeline but there needs to exist a way to convert that timeline into an FFmpeg command. Here’s the gist of how this is going to go:
This is in no way optimal. It uses significantly more resources than it should. But it is the only way I found that is able to encode all the data I have from the timeline abstraction.
Writing this into an FFmpeg is borderline trivial. I wrote a class that takes in a Timeline and will compute a command
of the form ffmpeg <input files> <filter_complex> <output_streams> <misc flags> <output_file>
The only part of the command that is interesting is the filter_complex
part. This is what takes the videos and mixes
them. This argument consists of many streams that are connected to each other. All resemble the
form (official filtergraph documentation):
[input_stream_1][...input_stream_n]filter_name=ARGS,filter_name=ARGS[output_stream]
A skeleton of the whole filtergraph looks like this
[1:a]anull[fa1]; // Apply audio filters (anull is a nop)
...
[12]setpts=PTS+0.0010/TB[fv12]; // Offset video and apply video filters (no filters present here)
...
[o12][fv13]overlay=enable='between(t, 0.0120, 0.0220)'[o13]; // Overlay to main stream
...
[fa1]atrim=start=0:end=0.8100,adelay=1|1,apad=whole_dur=7.3810[a0]; // Offset audio
...
[a0]...[a10]amix=inputs=11,volume=11[a] // Join audio streams
A full 8-minute video filtergraph can be viewed here.
Above is a partial render of a 5-minute video timeline. It’s pretty hard to get everything to fit in 1000 pixels. This is a look at how that timeline gets made.
The flow of the program is very linear as you can see below:
It’s pretty straight forward, so I’ll keep this section short.
The application is complete (in a sense) but it’s not 100% automated. Someone must run it for a new video to be generated. That just won’t cut it for me. I need a 100% hands-off solution.
The way images were being taken was extremely slow. It could take around 5–10 minutes for a single video to be imaged. Moving this task to the cloud could greatly reduce the time required for this task.
Given that the imager was already a Node.js project, the goto solution was serverless. All that was needed was an extra yml file, and the project was in the cloud.
And I wish it were that easy. It turns out that Puppeteer, the library used for controlling a headless Chromium instance did not play well with AWS Lambda. The size of the bundled Chromium executable greatly exceeded the max size allowed by Lambda.
The solution arose with chrome-aws-lambda. This bundles a compressed version of Chromium that satisfies the space requirements of Lambda. There are other libraries that also claim to accomplish this goal but after through testing numerous different solutions chrome-aws-lambda worked the best.
Now that the imager is in the cloud, the Java portion of the application must go too. Amazon ECS and AWS Fargate is a no-brainer for this. It supports running Docker containers and automatically provisions resources to meet the demands of those containers.
Building a Docker container is a trivial task that needs little explanation. The cool part is that Docker will host one free private repository. These repositories are cool because they can be configured to automatically build from GitHub. So when I push to GitHub, Docker detects the push and builds the docker container, and that container is pulled with the next execution of the ECS task.
In ECS, a task can be defined and scheduled to run on a certain interval. This allows me to run my generation as often as I please. I chose to do an execution at 9AM and 3PM MDT.
Running code in the cloud is not free, or at least that’s what I thought. There are some amazing free-tier offers from Amazon that make this application extremely cheap to run.
AWS Lambda charges in GB-s. My imaging program uses ~2GB of ram and runs in ~10 seconds. This program is then executed maybe 80 times to generate a video. The cost would be:
\[compute (seconds) = 10s \times 80s = 800s\] \[compute (GBs) = 800 \times 2GB = 1600 GBs\] \[cost = $0.00001667 \times 1600 = $0.027\]This is not mentioning that Lambda has a 400,000 GB-s free tier so I can generate ~40,000 captures per month cost-free (minus data transfer rates).
ECS is a bit easier to calculate. A video takes about 12 minutes to generate on a 2 VCPU 4GB ram provision. At the time of writing, the rates are as follows:
Item | Price |
---|---|
per vCPU per hour | $0.04048 |
per GB per hour | $0.004445 |
So the cost per video is the following:
\[cost = \frac{12}{60} \times ($0.04048 \times 2 + $0.004445 \times 4) = $0.0197\]So the total cost of making a video is about $0.05 ($0.02 with free tier) without factoring in data transfer costs.
This application could have been hacked together in a week. But I blew out that timeframe because I didn’t want to have to deal with the aftermath. I wanted to build this and check in on it every month. So many things had to be changed to support this idea. For instance, everything that interacted with an external API needed to have retries (spring-retry). Every external input to the program: Threads, Comments, Keywords, had to be tested with a variety of different inputs (quickcheck).
I found that this phase of the project dominated the majority of the overall development time. Possibly 80%????
This project was one of the most ambitious and complicated projects I had ever worked on. There are over 180 classes with a combined 7000 LOC at the time of writing this. It was a fun challenge. Here is the result:
This video was generated 100% without any interaction. The tags, description, thumbnail, title, and the first comment are all generated and uploaded via the application running the cloud.
So how about an FPV rig? To make this all work, I had to go and buy some parts.
Part | Price | Retailer |
---|---|---|
5.8G FPV Receiver | $17.84 | Ebay |
FPV Camera | $7.99 | Banggood |
BR2205 Brushless Motor X4 | $8.99 * 4 = $35.96 | Banggood |
RS30x4 30A ESC | $46.99 | Banggood |
PDB-XT60 | $6.52 | Banggood |
5.8G FPV Transmitter | $13.99 | Banggood |
F3 Flight Controller | $21.99 | Banggood |
5x4x3 Propellers | $8.59 | Banggood |
All of these parts total to $172.76. This gets closer to $100 if you choose to wait for all the Banggood items to ship from their CN warehouse, but that just takes too long.
I had to start with something. Getting anything made will act as a good starting base for me to work off of. That does not excuse me for this first design. It looks horrible.
So after some deep thinking, I went back to Fusion and tried again. This time I make something much more viewable. While documenting this, I noticed that I did not reference any existing design. Usually while building ariel machines a lot more than looks goes into the design. I am very lucky that this works.
I am going to guess that it took a collective 30 hours to print all the parts on my janky 3d printer. This one was a long haul.
Here is the largest part of the body being printed. This odd-shaped square thing took over 3 hours to print.
I started the assembly with the electronics. The main parts are the Electronic Speed Controller (ESC), Power Distribution Board (PDB), and the Flight Controller. The ESC is what will send current to the four motors. The PDB takes the current from the battery and delivers it to the other components. Finally, the Flight Controller takes in all the telemetry data from the on-board sensors and uses that to tell the ESC how much current to send to each motor.
It is common to take these three components and stack them on top of each other. This is referenced as ‘the stack.’
Wiring up the ESC.
ESC goes on the bottom, PDB in the middle, and the FC goes on top.
The body is composed of four arms connected to a central body with a small canopy on top. Each arm has 2 weak points in it so that it will snap in the case of a rough landing. In the first iteration of the design, I only included one joint.
Print two more arms and then the body is done. I’ll add the canopy in later.
Attaching the propellers.
For a battery, I used a 30C 3000mAh battery. I did not have to buy this as I had been using it with my 350 QX3.
I had some FPV of it flying around, but it appears to have been deleted. Next time I get around to flying this thing, I’ll post the video here.
]]>A few months ago I thought I would take a stab at making one of these on the cheap. Future me now realizes that these things are anything but affordable.
Here goes my try at making this. I found the cheapest parts I could find and slapped it all together in around 2 hours. Minimal effort required.
Part | Price | Total |
---|---|---|
KEDA 190KV Motor | $50 | |
Volador 42” Longboard | $60 | |
83mm Longboard Wheels | $27 | |
XCSOURCE DIY Electric Skateboard Longboard Kit | $24 | |
VESC | $95 | |
Controller | $34 | |
6mm Bullet Connectors | $7 | |
$296 |
Connect the motor and two batteries I had lying around, and that’s it. Perfect.
But it did not end there. I took it outside, and it made it about 300 feet before the motor mount came loose in the trucks. The reason is that the vibrations caused by the pathway made the setscrews in the mount come loose. This is the gist of the design (three screws clamping the mount to the circular trucks). Sadly, this also ruined my motor.
There was also another unforeseen issue. The belt that comes with the motor mount kit is an HTD 3M belt. The smaller teeth in the belt are not able to maintain a solid grip on the pullet while under heavy accelerating. There was a noticeable sound made when the belt skipped over the teeth of the pulley.
The most glaring issue of them all is that all the electronics are completely exposed. If a rock where to hit the ESC or batteries, there would be a good chance of braking something or possibly a fire. Some sort of enclosure would be a requirement.
Okay, let’s make some changes:
Part | Price | Total |
---|---|---|
Racerstar 200KV Motor | $42 | |
Sprocket Chain Wheel | $12 | |
12x24” Kydex Sheet | $17 | |
Sketchy 10s2p battery | $33 | |
10s Hoverboard Charger | $12 | |
$116 |
First things first. It was not smart to use these batteries. They are way too cheap to be legit and could have likely started a fire. I was pretty stupid to even touch these things. But here we go anyway.
Starting with the battery. It came looking like this:
I repacked it so the last four cells where on the side and not the top. It definitely looks like I know what I’m doing here.
Now came the motor and the chain drive. This was trivial. Just had to take off the old stuff and slap on the new. The enclosure was the hardest part. I cut the 24” Kydex sheet to size and put it in the oven for about 3 minutes, periodically checking the temperature with an IR thermometer. Once it was at around 350*f I pushed it around a 3d printed mold to get the desired shape. Some further tweaking was required by heating parts of the enclosure with a heat gun and pushing it into place. Then brass wood inserts where inserted into the board to provide a thread for the screws. Finally, everything was screwed into place.
This design also had major faults. I still have not addressed the motor mount issues and the enclosures look terrible. Predictably, the batteries gave out after six or so charge cycles (thankfully there was no fire).
This time I did some more research on lithium batteries, and I think I have it down. I got rid of the sketchy pack (safely) and decided to make my own from scratch.
Part | Price | Total |
---|---|---|
HM-10 Bluetooth Module | $7 | |
20 Samsung 30Q Batteries | $110 | |
103mm Heat Shrink | $7 | |
10S BMS | $10 | |
3/16” Ground Strap | $7 | |
$141 |
Making the battery pack was the main challenge of this revision. I was stressed out the entire time because shorting one of these cells means fire. Here was my plan for the battery.
The design doesn’t make a lot of sense now, but I thought it was cool at the time. It was also important to add a connector at the top of the two packs so that heat shrink could be put over each column of cells.
Also notice how the front truck is silver now. I snapped my front truck after running into a railing. Be safe!
Notice how the motor mount is not at a right angle to the trucks. That will be a problem. Also the battery weighs a fair amount. To have that much mass pounding on my poor enclosure while I hit speed bumps at 20mph means it likely won’t last.
And soon enough the enclosure and the motor mount where broken.
So the motor mount actually ended up ruining my trucks. The cheap metal ended up just snapping. Sadly, I don’t have a picture to share. Time to fix the motor mount!
Part | Price | Total |
---|---|---|
Caliber 50* Trucks | $40 | |
New Motor Mount | $60 | |
25H Chain | $11 | |
$101 |
I had hoped that spending top dollar on a motor mount would magically make all my problems go away. Sadly, I was mistaken. This new mount gave me even more problems than the last. First I was sent the wrong mount, then more serious problems arose.
I know you’re all saying I’m an idiot for using bearings as idlers for a chain. I also think this is stupid. But I wanted to see if it would work while my new mount was in the mail. It actually made a really cool note when I spun up the motor.
New motor mount came!
The original mount came with a M5 screw for the clamp. I wasn’t able to torque it down hard enough for the mount to stay on, so I drilled it out and tapped it for an M6 screw with a locknut.
Lots of Loctite were used in the battle to keep this damn thing from wobbling.
The battle was finally lost when it gave out on me while I was riding.
Well, now I’ve spent all this money and have nothing to show for it. Funny enough, this one guy on my campus saw I rode my board a lot and gave me a bunch of parts from one of his electric boards.
I am pretty sick of stuff braking while I’m riding. I want to go all out and try to make the most reliable board I can. I also was able to sell my old board to a friend and get a fair amount of cash back. Here we go again!
Part | Price | Total |
---|---|---|
TorqueBoards motor mount x2 | Free | |
TB 6355 190kv x2 | Free | |
TB Anti-spark switch | Free | |
TB VESC | Free | |
TB 16T Motor Pulley x2 | Free | |
TB 36T Drive Wheel Pulley | Free | |
TB 255mm Belt | Free | |
Atom Drop Through 41” | Free | |
Grip Tape | $10 | |
MBS 100 Wheels | $70 | |
Heat Gun | $16 | |
TB 36T Drive Wheel Pulley | $37 | |
TB 180mm Trucks | $60 | |
Battery Readout | $5 | |
HTD 255-5M-12 Belt | $6 | |
BMS | $11 | |
Kydex Sheet | $17 | |
Enertion NanoX controller | $35 | |
9” Heat Shrink | $12 | |
10S charger | $60 | |
40 Samsung 30Q Batteries | $190 | |
$529 |
I wish I had taken a picture of the board before I started cleaning it all up. It was not in great shape. There were lots of holes and small defects that I wanted to fix. Some wood filler and a finishing sander took care of fixing this.
Next, I wanted to plug the holes for the trucks. I know I want to top mount so these won’t be needed anymore.
Some sealant can’t hurt.
I taped everything up for painting. For some reason, I put the trucks on for this picture.
Here’s the new plan for the internals. One VESC is missing in this picture.
Now the batteries. I 3d printed this jig to assist me while soldering the packs.
Now a test run
I also made an aluminum enclosure for both of my VESCs. This should help a lot with the heat these things produce.
Now onto the enclosure. I spent 3 days 3d printing a new mold for the enclosure. It’s important to add some tape on top to help keep the heat away from the PLA.
Some quality time with a heat gun yielded some impressive results.
Then cut it out and sand the edges. I also drilled some mounting holes while I was at it.
Finally, add some holes for the motor leads and a few buttons.
I didn’t take a picture of the battery with the heat shrink on, but this heat shrink is awesome!
Finally, it’s done!
And yes, that is smoke.
]]>3d printers are really cool. You put plastic in, and it spits out highly detailed parts. Through building a 3d printer, I wanted to learn how these devices work and possibly save a few bucks. First comes the model.
This is the first project that I used CAD software to model. It’s actually the first real engineering project I embarked on. I didn’t know where to start, so I just went with the most flashy option I could find in terms of software. I started with a cracked Solidworks, there is no way you could get me to pay $4,000+ for a program I didn’t even know if I could use. Anyway…
Back to the model. Here are some of the worst models I have ever made. With having no previous instruction on how to use CAD, I would say these aren’t too bad.
This is a prime example of me having no idea what I’m doing. I just took some parts and joined them together. It did not come out that good.
This is not good. I don’t really know what I was going for with this design, but it offered a good lesson in how to create and join different components.
Okay. Let’s try this again. I needed a more rigid frame. For that, I went online and drew inspiration from different 3d printers that I found in Google Images. There’s no hiding that the Prusa I3 is the most popular printer design out there, so my design copies the major features of the I3.
This is quite a big leap in terms of the design. There are now three axes and a rigid-looking frame. It could use some improvements though.
This took me several hours to get right. I even had Solidworks crash several times while I was designing the model and had to restart more times than I would have liked to. That’s likely because Solidworks is written for high-end workstations with $10,000 graphics cards.
It’s still not that good. But for sure, an improvement over the last. We now have three axes, instead of one, and it looks a lot more rigid. There are some major problems though. I included the sidebar to show that the model is a mess. References are missing, nothing is organized, and there’s a random X-rod just hanging outside the model.
This is just Attempt 3 but with a few touch ups. This is something that I can make.
For materials, I decided on some wood from a local Lowes. It’s affordable and I can get as much of it as I need. I have access to a chop saw, so accurate cuts are pretty straightforward to achieve. The most challenging part is going to be making the parts precise enough so that it all fits together. My Dad has quite a bit of experience when it comes to woodworking, so I enlisted him to help me on my project. Thanks Dad!
It took a lot longer than I expected to measure and cut everything, but it was worth it.
I write these posts after I finish building something. Sometimes I forget to take pictures of some of the steps along the way. Here’s what I forgot to document. I made the X axis carriage, installed some linear rods, bearings, steppers, and belts. Just like that, I had the worst 3d printer ever made.
I didn’t have any 3d printer components yet, so I duct taped a pencil to the end and tried to draw some things. These are not that great as the belts aren’t even tight at this point.
There are some serious problems with this design though. The X-axis is supported completely by the threaded rod, the Y-axis is a piece of wood, and don’t get me started about the X carriage.
Back to Solid Works.
I am encountering a lot of trouble trying to backtrack through this project because I have this cluttered directory full of random files.
Back to the new design. I wanted more stability, the ability to hold a heated bed in a rigid manner and to throw away the X-axis. So I got to work with a now-assembled 3d printer. I also got rid of the wooden bed and replaced it with this new heated bead.
First revision that actually prints.
More support.
New X-Axis.
Revision 5 complete!
After revision 5 I took a long time looking at the downfalls of the current design and how I could make it better. One of the major weak points was the brackets that connect the Z to the X-Axis. Also, a Bowden Extruder would be nice.
I took several months to get to this design and spent well over $600 to get a printer that performs no better than a $200 printer. But I learned a lot over the course of this project. I am really happy that I was able to make something physical for a change. For so long I had been working on programming projects, it felt good to do something different for a change.
]]>