blog.aethena.art

Art Project Roundup, July 2026

Posted: 2026-08-01
Desc: Mostly code and math with artistic purpose


Fuck intros, I don't want to write an intro, I don't have time for intros, too much was achieved this month for formalities. No idea how long this will be, or if it even comes out on time, but I won't know unless I try. Let's just hit the ground running by talking about what I did first; Monochrome display support for certain blog posts. Raah

A screenshot of a Discord chat message, showing someone browing this blog on an e-ink tablet

See, around the start of the month, I was shown a video of a friend browsing my site through an e-ink tablet.

If you have photos of my blog being read through interesting mediums, or in neat locations, please send them my way, they are extremely entertaining and I love the visual recognition that people genuinely sit down with it to hear about what I've been up to, it's a very big source of energy to continue putting extra effort into these posts.

But refocusing, immediately it gave me a very impulsive idea to try to implement something to make these posts more e-ink friendly. In ways, this is really simple, but also massively complicated. Since I'm writing this part further in the future, let me just hop over to my website to...

An edited screenshot showing the corner of my last blog post being rendered incredibly blurry

Uh. Who smeared the corners of my blog posts ?

This took me a little bit to figure out, but seems to be a REALLY recent Firefox bug, which explains why I'm only seeing this on Nightly, and not on the Chromium installation I only keep around for testing issues like this. Though, it's Nightly 155, which that report says works fine...

Firefox 152 works fine.

Whatever...

An edited AI summary describing the Boox 5C, involving primarily aquatic wordplay

E-ink.

Specific device that's now the focus of this part of the blog is what was shown in the first photo: the Onyx Boox Air 5C. I have no skin in what is and is not a good device in this category for the time being, I don't care too much on if this is a good or bad device. My focus is entirely on e-ink accessibility, with that ONE specific device being the ONE known case I've witnessed of my site being read through the pretty fascinating tech of digital ink.

Like a lot of technical problems, I feel like "How does it know ?" is really vital to be able to answer. Specifically, how would the device know that there's a more user-friendly option of a page available to render ?

A picutre of an e-ink tablet, the Boox Air 5C

(Warning: Doing a LOT of assuming of the general "climate" of e-ink devices, and how I've been able to ascertain how they work, don't source me on this)

The easy answer is @media (monochrome). The reality is, it just doesn't care. At all. A lot of those devices don't care too much about if a site has a monochrome CSS @media rule for fairly annoying, hack-y reasons of assuming no site is willing to render in monochrome, and just insisting it is a normal display, then applying a bunch of contrast options outside of any CSS styling, in order to broadly reduce ghosting when scrolling. After all, that specific device is colored e-ink.

This was in fact a problem that stumped me hard enough, that I solved everything else around it before coming back to it at the end of the month with a very unsatisfying solution. See, there's two fairly easy solutions that don't work in this precise context. one is JavaScript, but this site doesn't use JavaScript because I do not feel like writing JavaScript, and enjoy keeping this site as a strange hybrid of web 1.0 crossed with modern CSS. The second solution is @media (prefers-color-scheme: dark). Problem with that is, we already know that a device like that won't respect light vs dark mode preferences, and may not even listen to any CSS rules with regards to light or dark mode, or could be overridden by the user. Hell, it doesn't even do regular black and white monochrome without being explicitly declared.

(Don't use me as a concrete quote for this kind of thing, it's just been bordering on impossible to find rock solid evidence on how it handles black and white vs color mode. If I could cite something better than Reddit, I would.)

So, we simply can't trust the device to give us any reasonable information to work off, which means it's on the user to either find a toggle somewhere, or trust them to use a device that states some kind of preference at all.



In that case, let's just make it first, and figure out the logic later.

Last month's post (and hopefully this one, if I'm not too far behind) do in fact have total light mode, high-contrast versions where all images are swapped with strict 1-bit alternatives stored alongside their respective style sheet that handles swapping them out with a toggle.

If you were curious, the .webm videos are even 1-bit too. Here's the ffmpeg copy-paste I brute forced the right parameters to. VP8 chosen for that little extra sprinkling of compatibility on top, like pop rocks.

ffmpeg -i INPUT-GOES-HERE.webm -vf format=gray,maskfun=low=128:high=128:fill=0:sum=128,negate -c:v libvpx -b:v 1M -c:a libvorbis OUTPUT-GOES-HERE.webm

All things considered, translating images to 1-bit black and white versions isn't too difficult to do for 90% of the photos I use. I suspect doing this manually as well gives me a lot of advantages over an automated system too, because I know what details need to be kept, and what can be approximated, I have full control over what becomes white or black



Now in the present moment while writing this, I had to decide on a solution to the problem of "If all of this has to be controlled user-side, how can I make this as painless and obvious as possible ?"

After sitting on it for a bit, I decided to take the most "trusting the system to work as intended" approach, and just use @media (prefers-contrast: more) like a sane shark, partially because then I can do the cool trick of bolting filter: invert(100%); to all my images. Amazing. Riveting. Wonderful. The crowd goes wild.

The plan is to take it bit by bit, and go back through posts chronologically, and do a bit of manual 1-bit converting for past stuff, along with smoothing out general style preferences, and defining what I want light mode to look like, given that since its inception, I've been only thinking about this site's look in one strict color theme. Basically, if I cover ALL preferences, then it has to default to SOMETHING correct enough.

If you view this page on a device that prefers light mode, prefers more contrast, or is somehow monochrome, then @media (prefers-color-scheme: light) or (prefers-contrast: more) or (monochrome) should trip. A temporary solution that hopefully covers enough bases for the time being, until I lock down some bigger site updates next month



After that, I felt compelled to revisit an idea I had in the back of my mind, but didn't know how to execute on.

See, I love watches. Still do. But, the one on my model is an analog one, and I know how to rotate a bone 360 based on a time value. This used to be a very complicated concept to execute on, but is now second nature. A digital watch however, isn't as...linear.

There's a lot of strange ways one could drive animation data to change something based off the time value passed in VRChat through OSC. Originally, I tried animating a scale and tile offset to control the state of an hour and minute animation based on that data, and it never quite worked the way I wanted it to, and it was only upon reviewing other non-functioning VRChat watch assets that I realized, clothing and accessories used blend shapes to adjust size and fit



Blendshapes...hear me out.

A digital watch has four digits. Two for hour, two for minute, zero to nine on all. So 40 blendshapes for four digits on a segmented display, you just lift the segments based on the number you want.

So I looked to the information superhighway for info about segmented numerical displays, and came across a cool video I've definitely seen in my recommendations at least a dozen times, but never felt compelled to click. Wound up finding out that it was absolutely up my alley, and that I at some point also had the same level of passion about segmented displays, but never went that far with it



This compelled me to design my own segmented display, because how hard can it be.

The answer is, harder than I expected, and what you're looking at in all of its horrifying, janky, inefficient glory is mine, based off my own handwriting. It is a 15-segment display that uses what I originally intended to be brush strokes, but distilled down to a curved teardrop shape, and circles. I have other designs sketched out that I shall keep close to my chest to use in future watches. The readability of this one though was awkward at first, but didn't take much time at all to...

A screenshot from Warframe, highlighting the user and their chat messages that pointed me towards Tennogen

Oh. It just...works ? It's really just that easy ?

Alright then, wicked. We have a working prototype !

...is what I would say, but we have a minor problem



See, I began making a few alternate prototypes for a couple friends, and quicky realized "Oh. VRCOSC is a Windows program. Only Windows."

So, now I had another question to answer, another problem to solve. The program that I use to drive my current watch animations is not cross-platform, and the alternatives I were able to find felt...lacking. Didn't have all the options I needed.

At this point, I had a choice. Shaders made me...perhaps not comfortable with code per se, but it certainly didn't feel insurmountable as a subject. What if I just...write my own damn program that works the precise ways I need it to. Design it with my needs in mind first



So anyway I learned Rust, and wrote a program to communicate parameters over OSC to VRChat over port 9000.

It wasn't even that hard either. As it turns out, my intro to working with code was pretty lackluster, and the questions I had at the time, I had no satisfactory way of being answered. Assumed that I just didn't "get" code, that you had to either learn how it worked from a classroom, or it was a skill you just intrinsically understood young.

Neither were true, and the logic behind it clicked extremely fast. It helps that what I want to do isn't immensely complex, we're just taking the system time, translating it to different amounts of a normalized variable, then passing that over to OSC. The OSC somehow was the least tricky part, because Rust has a library for that, and this time, I know why I'm using a library, I know its purpose, and I know how to do what it does if I didn't use it.

OSC communicates in packets that have to adhere to a strict length, divisible by 4. That's more or less the purpose of rosc for this, communicating and structuring data. A strange thing I've started noticing as a recurring pattern, especially once I found out that Apple Metal also requires four-byte alignment, was that we've been aligning bytes for memory performance purposes for decades, with most of this being handled automatically. Here's a neat Wikipedia section on the subject if curious



It was at this point that I feel I had crossed over the event horizon of "Am I actually a programmer", where this was far more undeniable now. Upon showing this to the very same friend who pointed out to me originally that VRC-OSC was not Linux compatible, they then proceeded to show me how git works.

I will slam this down upon your table for you if you're not aware of git, or you think it's only for programming things, but git is not just a programmer's tool. It's just version control that remembers changes to a file. So long as that file isn't a binary, you can see changes over time, and log them as commits, giving you something pretty damn close to save states. So long as you remember to "save", that is



Now, the animation controllers for making this all work are a bit...tricky. The less fun, harder to handwave kind of tricky.

Some rapid fire notes about it all. One is parameter mismatch is a very real technique that absolutely works, two is that blend trees are very powerful, and the final one being that VRChat's Contact Receivers, especially "OnEnter" are somewhat janky.

The buttons on my watch specifically use Constant contacts that are looking for a sender that's "marked" as a finger. When the two contacts overlap, the button's boolean parameter is set to True. When the finger collider leaves, it becomes False. With this kind of logic, we can set up gates to check that one button is True, and that all others are false, which moves the animation state forward to a buffer. When you release your finger, the buffer updates, as True becomes False.

Is this the best way of doing this kind of thing ? Probably not, but it's a hell of a lot better than VRChat's much more hack-y feeling "OnEnter" that only updates for one frame, which broke CONSTANTLY in my testing. Logic gates made more sense, and would only break when someone was trying really hard to do so. Plus, you can hold down a button, and feel like you're actually doing something, instead of simply moving your finger to an area that turns something on. Skeumorphism, that's the word I'm looking for...

Oh yeah, I didn't talk about the meetup yet



Loved it, no notes. I'm getting better at existing around other people, things feel a little smoother.

Well, not technically. I mean, in an actual technical sense. Virtual Desktop feels like the most infuriating black magic bullshit at times, where I can put my headset on an hour before the meetup, and everything's fine. Ten minutes before though, and suddenly I get massive encoding latency out of nowhere that takes multiple computer restarts to "fix" (I don't think they fixed anything, I think I just got lucky, but you can see the problems arise in the top picture, with me standing in an aggressively awkward pose in order to minimize the maddening latency, knowing it could crash if I turned my head too fast)



There were also certain feelings I had validated, that I hadn't had validated before.

I like my long, isolated, low energy hangout talks about vulnerable stuff. I don't like worlds or social activities focused on games, I'm too competitive, too eager to understand it all and master it. The way I talk over text, and how I act in VR are more similar than I realized, and just thought that my stilted awkwardness in VR was some critical failing of who I am when compared to others.

It isn't. I'm just...me, and I can hold some amazing conversations very effortlessly, but I'm saturated in substance, not style. I can't strike up a conversation to save my life, but if someone starts asking me honest questions with a desire for genuine perspective, they're going to get some pretty unfiltered and precise responses, and that can be a really valuable perspective to have around. Just because it doesn't fit into how I see others thrive doesn't mean that I can't also exist in my own ways just as comfortably



The shader was a hit too, and something I didn't expect to find comfort in, was being able to go kinda-sorta invisible on command. I adjusted one of the parameters to act as a "shitty cloaking" system, where I could swap all materials to my shader, then control my transparency on the fly, allowing myself to be semi-invisible, or completely invisible.

Well, sort of invisible. The shader still wrote to the depth buffer, so if a nameplate was behind my body, it would be culled, even if I was completely transparent, as I use ZWrite On, and VRChat nameplates use ZWrite Off, which means my shader writes to the depth buffer, while nameplates don't. An easy fix that I'd implement later on, because...



I was sent this little video. A friend of mine was tinkering with GLSL shader stuff, and I found it quite novel to see a mesh rendered not in wireframe, but as something like a point cloud.

Since I've been on a roll, I asked myself yet another question of, how do I go about rendering only the vertices of a mesh. After all, we have the data, because...well, we have the vertices, we quite literally call it a fragment-vertex shader. But in HLSL, or rather Unity's strange proprietary implementation of HLSL, it's just not that simple.

See, we have, or rather had, PSIZE in DirectX9, but it was removed in DirectX10, which suddenly makes the task of drawing something to represent where a vertex is in 3D space somewhat complicated. The solution I smashed my head against for long enough, was adding a Geometry shader to a second pass of my personal shader, creating a weird Geo-TNA shader thing that performed well at first, but quickly showcased a lot of technical issues and limitations of the geometry shader, as it exists in a strange limbo of being outdated tech, but also has yet to truly be replaced by something better like tessellation, compute, and mesh shaders.

Or if they do a better job, I can't use them due to being locked into DirectX11 by way of VRChat. Besides, they've been around long enough to have substantial documented examples to reference



Here's the main page I pulled the initial logic from. The only change ?

float dist = distance( i.uv , float2( 0.5 , 0.5 ) ) ;
if ( dist > 0.5 ) discard ;


We draw two triangles to make a quad, then discard the corners, giving us circles for every vertex.

I'll admit, this was a very surreal effect to see, a point cloud that I knew could be distorted just like the vertex shader part of what I was making, a whole new avenue for ideas. Ideas that I would exhaust to the best of my abilities for the rest of the month.

What, you thought this was going to be a shorter blog post, just because it was only one month instead of three ? You beautiful, ignorant fool, you have no idea how far I'm going to take this idea for the rest of the month.

(Though writing about it for this post may be cut short, as I've already been doing a lot of documentation writing as-is)

A picture of mine, of a rave at Megaplex 2024, a furry convention

We can draw brand new geometry, and the underlying logic won't differ any from the fragment or vertex parts, which means, we can distort it. But how do I want to distort it ?

Well, I've been having the time of my life learning about mathematics through shaders and feel that I have a basic understanding of trigonometry down, so what else is there...

What about like...a radar sweep ? How would a shader calculate the effect of a radar sweeping across something like a sphere, because if we had an answer to that, then we could start doing some extra math to distort drawn circles based on those sweeps.

The best mathematical answer I could find, was calculating spherical coordinates using phi, and theta

A side by side composite with a picture from VRChat on the left, showing how my rave outfit

Unsurprisingly, others have solved these kind of things before us, and we quickly learn that this is a job for

float phi = atan2( dirNorm.z, dirNorm.x ) / UNITY_TWO_PI + 0.5 ;

...and float theta = acos( dirNorm.y ) / UNITY_PI ;

Fun thing to note, we use atan2() because atan() calculates an angle between -90 and 90 degrees, while atan2() gives us the full range of -180 to 180 for a full 360 rotation that tells us which quadrant a point is.

In the middle of all this, in a freakish, lucid haze where I wasn't thinking about the numerous ways to map and translate coordinates in a 3D space, I got a flashback to a feeling I got around...early 2023 ? I was late to the chess wave that possessed modern culture as if [medieval football](https://en.wikipedia.org/wiki/Medieval_football) was back with a vengeance, but when I did dive in, I felt a sensation that felt unique to me, but I know I had felt before.

It was the feeling of something almost like addiction, but one for knowledge. When you sample from some wellspring that just causes you to feel that one-two hit of fascinating information, and the fact that it does. Not. End. No matter how deep you go, there's always more, you will NEVER be able to completely and fully retain it all, and yet you just want more, it's a desire to gorge on knowledge even at your own detriment. Even when you're exhausted, you just can't stop, you can't control yourself.

I had to finish this damn shader



Now, given the fact that I was doing some fairly heavy mathematics in a geometry pass, something infamous for poor performance, I started to get paranoid about what my average framerate was in a worst case scenario.

In enters RenderDoc. It isn't as precise as I'd like, and it took me a bit to get used to, but I found that I could hook into VRChat and bypass their Easy AntiCheat with a few launch options. Specifically...

--no-eac --no-steam --no-vr --watch-worlds

With this, we get access to a weird pseudo-offline mode, where we can capture all the calls made in a single frame to break down their timing. I can't really pinpoint what in my code performs poorly with it (or if I can, the "how" went over my head), but I can see if the first or second pass is the worse culprit for frame timing, because at this point something felt incredibly wrong with how little optimizing I've had to do in order to get this working well.

Ultimately, many of the performance issues I solved much later on with a distance based cull that allows the user to decide where they stand on the performance they want, as with a geometry shader, it doesn't matter how optimized my code is, if the end user smashes it of a 200k tri mesh. Best I can do is my best, and to inform.

This also meant for the time being, I could ignore polynomial approximations of something like atan2(), which from my understanding, isn't really that massive of a performance problem in this context, especially as this is only done once. We shall seeeee, we shall see...

Bonus links about optimizing trigonometry I found and collected thrown here after a rewrite of this post to remove a lot of prefacing about me being terrified of being critiques harshly because I'm still learning and may not do everything absolutely perfectly. Calculating atan without atan, Godot approximating atan2 in-engine, creating a faster asin plus their other approximations in a raytracing project they were working on, their own reference to faster asin() from Nvidia, and the 1964 textbook Nvidia cites on polynomial approximations for this very thing. I promise, I care deeply about optimization, I'm doing my best, there's just a lot to learn still and I didn't grow up knowing what the best way to learn was.

Double super bonus link to a Tiger Electronics Game.Com commercial that sums up my fear of asking questions, and anticipating a hostile response



This is all to say, technically I did finish my shader by the end of the month, I simply didn't finish writing documentation for it, and couldn't lock down a reliable perspective from anyone in time about how to like...go about actually releasing it. The where, any possible complications. It'll be something that will be sorted out likely a few days after this post goes live, and the intent is to release it as something where you pay what you want. After all, I made this shader for...well, me, and this entire time, it's been a learning process and passion project instead of a...thing, a product designed to be bought.

I'm just happy people that see it seem to like the effect, and I'm hoping that over time, I can iron out any major problems it may have that both I, and the friend I asked to test the usability of it, may have possibly missed. Hence the desire to also have documentation written, good practice for when I actually do release something intended to be a product or tool



Cutting this blog post off shorter than its intended length due to checking the word count, the clock, and wanting to call it somewhere. I need a break, a real one. I'm gonna go play Stalker Anomaly, or Baldur's Gate 3, or go binge some show. Once I have that documentation finished. Perhaps next month, it'll...

Nah. it won't be more chill, I'm not built to be chill deep down, it'll still be just as passionate, and hopefully just as exciting

🧡

  - Aethena, 2026-08-01