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

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.

  1. 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.

  2. 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.

  3. 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.

Exterior view. The native build on its first successful output window.
Interior with hand tracking. MediaPipe native binding drives the particle response in real time.
Interior with soundscape meter. Audio levels visible beside the particle field as an early debugging aid.
BEFORE · Web prototype

Three.js in the browser. Three pillars technically running, but latency, performance, and visual ceiling all capped the experience.

AFTER · Native application

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

End of Week 4. The first native application run, rough but functional. From here the entire project lives outside the browser.
End of Week 4. The first native application run, rough but functional. From here the entire project lives outside the browser.