WEEK 04 · 2026-02-02 ~ 08
The migration
Rebuilt the whole application as a native Python desktop program in two days. Lost all the Three.js conveniences, gained direct access to the GPU. The safer option was not the right one.
Last week ended with three walls and one conclusion. The browser was not going to carry this project. I spent the weekend thinking about alternatives and made the decision before Monday. Python for the main logic. ModernGL for rendering through OpenGL 4.3. MediaPipe running directly on the machine. OpenAL for spatial audio. The whole stack would move off the web and onto a native desktop application.
I expected the migration to take a week. It took two days. Not because it was easy, but because the scope of what had to move was smaller than I had feared.
THE STACK
The new stack was not chosen for novelty. Each piece solved a specific problem from the previous week.
TECH STACK · DATA FLOW, TOP TO BOTTOM
- Raw input from the visitor's hand, mouse, keyboard, and webcam feed.
- Google's hand tracking model runs directly on the CPU. It returns 21 landmarks per hand at roughly 30 frames per second with no browser overhead.
- The top-level application code. Handles scene state, audio playback, and coordination between MediaPipe, ModernGL, and OpenAL.
- A thin Pythonic wrapper over OpenGL 4.3. Exposes compute shaders, SSBOs, and advanced texture formats without the low-level C boilerplate.
- The rendering API. Compute shaders and Shader Storage Buffer Objects are the two features that enabled large-scale particle simulation on the GPU.
- NVIDIA laptop GPU, 8 GB VRAM. The hardware target for the project's performance budget.
- The output window. 60 frames per second with hundreds of thousands of audio-reactive particles.
Python was the language I could write fastest. It is not the fastest to run, but for application logic that is not the bottleneck. The bottleneck is the GPU, and ModernGL lets Python talk to OpenGL 4.3 directly. OpenGL 4.3 is the minimum version that supports compute shaders and Shader Storage Buffer Objects, the two features I needed for large-scale particle simulation. MediaPipe has a native Python binding that runs on the CPU without a browser layer, which removes the latency problem at its source. OpenAL handles 3D positional audio in a way that pairs well with the first-person interior scenes planned for later weeks.
TWO DAYS, THREE PAINS
The migration was not smooth. Three separate difficulties showed up, and none of them were about running out of time.
-
Losing the Three.js conveniences
Three.js hides a lot of work. Scene graphs, camera matrices, the default render loop, the primitive geometries. In Three.js I could write
new THREE.Points(geometry, material)and a particle system existed. In ModernGL I had to write the vertex buffer, the vertex array object, the vertex and fragment shaders, the uniform uploads, and the draw call, all by hand. Each one is a small amount of code. Together they are a whole day of re-implementation before a single particle appears on screen. -
Rebuilding from graphics primitives up
The second difficulty was deeper. In the web version I was composing features. In the native version I was composing primitives. A particle in Three.js is an object with a position and a material. A particle in ModernGL is a float in a buffer that a shader interprets according to how I wrote the shader. There is no built-in notion of what a particle is. I had to define it, and then define what drawing it means.
This is more work in the short term. It is also why the native version could eventually go places the web version could not. The abstractions that Three.js provides were also the abstractions that were limiting me.
-
Thinner documentation and less AI help
Three.js has a large community and an enormous amount of example code on the internet. AI coding assistants know it well. ModernGL is smaller. Python with OpenGL is smaller still. When I asked an AI assistant to help me debug a compute shader issue, the answers I got back were often plausible-looking but wrong. The training data for this corner of the stack is thinner, and the feedback loop slowed down because I had to verify more of what the AI produced.
I had used vibe coding as a primary method throughout the project. This week was the first time that method pushed back.
Git carried me through all three. I committed after every small success. When a change broke the build, I rolled back to the last working state and tried again from a different angle. This was not a lesson I had learned fully yet. It would take losing work later in the semester for the habit to fully stick, but the groundwork was already being laid this week.
THE RESULT
By the end of the second day the native application opened a window, loaded an audio file, and drew audio-reactive particles on screen. The visuals were rougher than what Three.js had been producing. No bloom out of the box, no soft alpha blending by default, particles rendered as hard dots. But the latency was gone, the frame rate held steady at 60 with far more particles than the web version could handle, and I had direct access to compute shaders for everything I wanted to build next.
Three.js in the browser. Three pillars technically running, but latency, performance, and visual ceiling all capped the experience.
Python with ModernGL. Rougher visuals, faster response. The ceiling that existed in the browser is gone.
The application was now on the stack it would stay on for the rest of the project. What came after was not going to be about platform choice any more. It was going to be about what I could actually build on top of it.
FINAL RESULT · END-OF-WEEK STATE