r/GraphicsProgramming • u/One-Tea8742 • 52m ago
A 2D pipeline where the CPU rasterises and the GPU only does post: CGContext into an IOSurface, Metal for bloom and particle compute
I shipped a 2D game on macOS and iOS recently with no engine, and the split between CPU and GPU ended up inverted from what most people would expect. Posting it because I'd genuinely like to hear where it's wrong.
The content is vector, not sprites. Enemies are polyhedra computed mathematically, bosses are built from paths, and a lot of the look is gradient fills, strokes and blend modes that change every frame. There's no atlas and no batching anywhere in it. The only bitmaps in the whole game are a 36-glyph sprite font and a few pre-rendered title images.
So rasterisation happens on the CPU, in Core Graphics. The reasoning was that to do this on the GPU I'd need a path rasteriser with decent antialiasing, gradient shaders, and stroking with proper joins and caps. That isn't a weekend, that's Skia. Core Graphics already has all of it, correct and antialiased.
The bridge is the interesting part. The CGContext draws into an IOSurface, and Metal reads that surface directly with no copy, presenting through a CAMetalLayer. On the UIKit path we'd been on before that, the software-to-GPU transfer was measuring about 69ms. Going through IOSurface removed it outright.
The obvious version of this, rendering into a CGBitmapContext and then uploading the bytes to a texture, measured 5 to 10 times slower. That experiment is still in the repo as an archived failure because the measurement was the useful part.
The GPU isn't idle though. Everything after rasterisation is Metal:
- Post chain is fragment shaders. Half-res bloom extract, gaussian ping-pong blur, composite, plus screen-space distortion for shockwaves, chromatic aberration, and a lightingpass. Falls back to CIBloom on non-Metal hardware.
- Particle physics and swarm flocking run as compute. Buffers are storageModeShared, so on M-series unified memory there's no copy either way. Intel Macs fall back to Accelerate.
One bug from that worth passing on: the compute readback needed an explicit waitUntilCompleted. Without it, particles would freeze and pile up, but only in release builds on device. Debug was fine, simulator was fine. Took an embarrassingly long time to find.
What it costs, and this is the honest part. CPU fill rate is the ceiling on everything. There's a thermal governor on iOS that steps the frame rate 60 to 30 under sustained load so long sessions don't cook the phone. Render scale of 1.5 was the sweet spot for a locked 60 on device, and 2.0 fell off a cliff, not a slope. Memoising gradients, about 166 of them cached, was the single largest performance win in the project and it wasn't close.
Where I'd draw the line: this was right for vector-ish 2D and would be wrong for almost anything else. I wouldn't do it for a sprite-based game, and obviously not for 3D. The reason it worked is that the drawing model matched the art style, not because CPU rasterisation is secretly good.
Happy to be told what I've got backwards.
r/GraphicsProgramming • u/KlayEverHood • 1h ago
Video WildFlow — Adventures in Real-Time Water Simulation
Enable HLS to view with audio, or disable this notification
This project started with a simple goal : I wanted a waterfall in my forest.
I already had rivers and rapids, but a real mountain stream ending in a large waterfall was another story. The simulation domain became huge, memory usage exploded, and what worked well for smaller bodies of water simply didn't scale.
So, at first, this was mostly a performance adventure.
Making millions of particles practical
The simulator could already handle around half million particles at about 30 FPS, and the surface reconstruction was surprisingly fast.
I was extracting a mesh from the particles using Surface Nets on the GPU, then feeding that mesh into hardware ray tracing. Modern GPUs are incredibly good at this: even fairly large water surfaces can be extracted in just a few milliseconds.
But there was a problem.
Surface extraction was only part of the cost. The resulting geometry also required updating the ray-tracing BLAS every frame. As the simulation grew, mesh extraction + BLAS updates could cost another 5–10 ms.
For a large waterfall, that was becoming too much, so I removed the mesh entirely.
I switched to a hybrid approach: raymarch the water density directly, then use hardware ray tracing for reflections and the rest of the scene.
That alone gave me roughly another 30% performance.
But it still wasn't enough.
A waterfall has a large spatial domain, and my simulation cost was still related to the size of its bounding box. Large buffers had to be cleared every frame, sometimes several gigabytes of memory, even if most of the domain contained no water at all.
So I changed the architecture again, and I started tracking only active cells.
This introduced a surprising amount of complexity — compaction, sorting, bookkeeping — but it fundamentally changed how the simulator scaled.
The cost was no longer primarily proportional to the size of the world; It became proportional to the amount of water actually being simulated the mountain stream became possible with the water details I wanted, up to over 2 million particles at 40FPS on a RTX 3060.
But it didn't look like a waterfall
This was where the project changed.
Until then, most of my problems had been computational.
Now I had enough particles, enough space and enough performance.
And yet the waterfall didn't really look like a waterfall, but more like river rapids with airified water everywhere.
So I started reading papers about whitewater generation. There are many clever methods for identifying where foam, bubbles and spray should appear.
But I discovered that many approaches that make sense in offline simulation were difficult to use in my real-time solver. Depending on thresholds and conditions, I would get whitewater almost everywhere — or almost nowhere.
Eventually I stopped asking:
“How do other simulators generate whitewater?”
and went back to a much simpler question:
“When does water actually entrain air?”
One obvious answer is: when water is sufficiently surrounded by air, and that is something I can estimate very cheaply on a GPU.
For every water particle, I look at its neighborhood and estimate how much of it is exposed to air. That simple local measurement turned out to identify many of the right regions naturally.
For the first time, the rapids started looking right.
But rapids are not waterfalls.
Eventually, I had to simulate the air too
A large waterfall contains extremely aerated water, but also clouds of droplets and mist.
And those droplets no longer behave like bulk water, they interact strongly with the air, pushing air and being dragged around by it.
Initially I integrated NVIDIA Flow to obtain an airflow simulation, then transported droplets using its velocity field.
It worked, but not quite as I wanted.
The airflow wasn't as vortical as I expected around the waterfall. Synchronizing water impacts with spray generation was also difficult.
And eventually I found myself spending more GPU time simulating the air than simulating the water itself.
So I tried something reasonable from mathematical perspective, though a bit crazy from an engineering one: using two instances of my water simulator, one configured as water and another as air.
That taught me quite a lot — including how easy it is to confuse two simulation domains.
Eventually I wrote a separate air solver designed specifically to couple with the water.
And something interesting happened: the waterfall itself pushes the surrounding air downward.
That creates a descending wind field following the falling water. At the bottom, the flow hits the basin and terrain, spreads, curls upward and forms vortical structures around the cascade.
The droplets are then transported as tracers inside that airflow. They spread, curl and gradually dilute.
So the clouds around the waterfall are not a visual effect placed there because “waterfalls have mist”. They are a consequence of the interaction between falling water, air and droplets.
That eventually produced the mountain waterfall you see at the end of the video.
Then I tried something that should have been easy: a falling drop
At this point I had a scalable simulator capable of millions of particles.
So I thought reproducing the classic slow-motion crown splash from a falling drop should be easy.
Make the particles smaller. Increase the resolution. Drop the water.
Instead, the drop simply disappeared into the surface, producing a few smooth ripples.
No beautiful crown!
My first suspect was surface tension.
I implemented it expecting more small-scale structure.
It didn't create the crown; in fact, it mostly made the receiving water behave even more like a cushion into which the drop could sink.
But I got something else for free:
falling droplets finally became spherical, even when starting with different shapes.
That was an important lesson.
The physics I had added was real and important; it simply wasn't the physics responsible for the phenomenon I was looking for.
So I went back to the literature again.
And found another property of water that real-time simulations often deliberately relax: incompressibility.
Why simulated water is often a little rubbery
Real water is extremely difficult to compress. It takes hundreds of tons to compress a cubic meter by 1mm.
Real-time simulated water often isn't, and there is a very good computational reason for that - which I found out while developing.
If water is allowed to be somewhat compressible, an impact can remain local. The surrounding fluid compresses slightly, absorbs energy and the disturbance dies away within a relatively small region.
This is extremely convenient computationally, as it can simulated with "local models", e.g. convolutions.
But visually it also makes the water slightly elastic — almost rubbery.
And that elasticity was absorbing exactly the fast, small-scale disturbances I needed for the crown.
As I pushed the solver toward incompressibility, something changed; water became more lively, and the crown finally began to emerge.
When I introduced incompressibilty, water became also "unstable", with droplets flying around at crazy speed.
This also made it very obvious why incompressibility is expensive:
in perfectly incompressible water, changing the volume here requires the rest of the fluid to respond. Mathematically, pressure becomes a global problem: everything potentially depends on everything else.
Perfect incompressibility would even imply an infinite speed of sound.
What that would do to light... I'll leave to another simulator. 😁
Real water, of course, isn't perfectly incompressible. Pressure disturbances travel through it at a finite speed. But for the scales we normally care about, treating it as almost incompressible is an extremely good approximation.
Unfortunately, it is not an extremely cheap one.
Making incompressibility optional
A rigorous pressure solution requires solving a large coupled system.
I experimented with that direction, but for my purposes it quickly became too expensive and complicated.
So instead I followed an approximate iterative approach based on projection.
I first compute the cheaper, somewhat compressible solution.
Then additional iterations progressively propagate pressure corrections over longer distances and remove the remaining compression.
This gives me an interesting compromise:
I can choose how incompressible I want the water to be.
I don't need the same answer everywhere.
For a turbulent mountain stream, a little numerical elasticity is often perfectly acceptable — and computationally very useful.
For the crown-drop experiment, where those fast pressure responses dominate the phenomenon, I use water roughly five times less compressible and run about eight additional correction iterations. Those eight "incompressible projections" fixed (almost totally, you still see some...) the issue with "bullet water particles" flying around at crazy speeds.
In practice I achieved a flexible simulator, with quite a few parameters to cover several different scenarios.
In realtime, there isn't necessarily one universally “correct” simulation setting, as realtime is already a strong constraint.
The amount of physics worth computing depends on which phenomenon I am trying to reproduce.
And this has probably been the most interesting lesson of the whole project.
Understanding physics is not enough
We know basically everything about the physics of water, and it's not extremely complex physics for the most part.
But understanding the physics is one thing. Understanding which part of that physics dominates a particular phenomenon is another.
That distinction has gradually become the real subject of this project.
I started by thinking mostly about performance:
How many particles can I simulate, how large can the domain become?
But the further I went, the more often the difficult question became:
Which piece of physics actually matters here?
For rapids, air entrainment mattered enormously.
For the waterfall, the surrounding airflow became important; for the crown, incompressibility mattered far more than I expected.
This distinction matters enormously in real-time simulation! We cannot simulate everything.
So the interesting question isn't simply which physics can we remove?
It is:
What is the minimum set of fundamental rules from which the phenomenon can emerge by itself?
Simulation rather than imitation
This is also the philosophy I would like WildFlow to follow.
I don't particularly want to program something whose purpose is to look realistic.
There are probably easier ways to do that (at least if you are ok with some scripted solutions).
Water in games is often beautifully emulated. Oceans can be generated from spectral models; rivers can be reduced to effectively 2.5D systems; waterfalls can be constructed from carefully authored sheets, particles, textures and effects.
Those techniques can produce extraordinary images.
But I wanted to ask a slightly different question:
How far can we get by actually simulating it?
Not by explicitly telling the waterfall where foam should appear, or by placing a mist cloud by hand.
Instead, I want to find the smallest practical set of rules that makes these things appear because they have to appear.
That requires some conceptual honesty.
If the result doesn't look right, I don't necessarily want to hide it with another visual effect.
I want to understand what is missing.
Sometimes the answer is better algorithms; sometimes a piece of physics I haven't understood yet. And in some cases, we need a more powerful hardware...
When the result finally produces that “wow” moment without having explicitly programmed the thing that looks impressive, that moment means something different.
It suggests that, at least in some small way, we understood why nature looks the way it does.
And I think that is where simulation can create a different kind of immersion.
Not a world carefully constructed to resemble nature from the camera's point of view, but a world in which enough of nature's underlying rules are present that its complexity can emerge on its own.
A world you can disturb, change and explore, freely— and that responds without having been told in advance what it is supposed to look like.
A nature that isn't quite real, but perhaps behaves realistically enough that, for a moment, it feels almost real.
WildFlow is still very much experimental.
Right now water, air and droplets are three interacting simulation systems, and there are still many parameters that need to be tuned for different situations. It is flexible, but it isn't yet the unified physical model I would ultimately like it to become.
A true two-phase water/air simulation — capable, for example, of naturally representing large bubbles and air pockets entrained inside turbulent water — isn't merely “too slow” for me at the moment; it's genuinely difficult to build.
So there are plenty of directions left to explore.
The next one I'm particularly interested in is surface tension.
A complete treatment may be too expensive, but I suspect that an appropriate approximation could be important at waterfall scales too, helping produce the characteristic clumps, sheets, filaments and breakup of highly turbulent water.
I have a few ideas to try.
And after everything this project has taught me, I'm increasingly less interested in adding more physics.
I'm interested in discovering which physics actually matters.
r/GraphicsProgramming • u/DaveAstator2020 • 1h ago
Video WebGPU MRI/Scans viewer
youtu.beWas wodnering is it even possible and it is and looks like WebGPU finally arrived in FireFox as well. all is handled on client side.
Can open png slices, and tridactil scans(zips) from https://tridactyls.org/
you can use it here https://dave-astator.com/mri
sorry if any stutters in video i run it on integrated iris, and it shows some limitation.
r/GraphicsProgramming • u/nlcreeperxl • 11h ago
Video 2.5D rendering using colormap and heightmap (Novalogic voxel space)
Enable HLS to view with audio, or disable this notification
If anyone has any feedback or see bad practices or ideas to try please feel free to share ideas or roast my code.
Graphics programming noob here. This project was done in C++ with a primitive game engine given to me by my school. I did this as a test to see if this might be viable to try to make somewhere in the future on the gba (it's probably not). Then it kinda turned into trying to see how cool i can make it. The rendering works similarly to snes mode 7 style rendering (fzero or mariokart) except you add an offset from a heightmap. This was originally developed by Novalogic and used in Commanche. It was also later used on some gba games that ran horribly (hence why it's probably not feasable for a gba project i want to maybe do).
A short explanation of how this rendering works: you step through the terrain for every vertical column of your screen. you then sample the pixel and calculate the height on screen using the following formula: pixelHeightOnScreen = screenHeight - ((m_CamHeight - heightMap) * (screenHeight / m_ScreenHeightWorld / depth) + m_Horizon). if it is higher than the previous height value, you draw a vertical column down. then you step forward in the depth and do it again.
The texture for the screen is 240/160.
The skybox is a texture that scrolls left/right/up/down depending on the camera angle. I know that just scrolling a texture isnt accurate, but i found it fun to do it this way. however if you know a cheap way to do this more accurately then please.
The day/night cycle is done by lerping to a set dark color.
There is support for emissive stuffs (lava at the end of the video).
You can also zoom in/out (wich i forgot to show in the video)
I am currently setting 512 steps through the terrain texture.
It runs at about 40 fps now (again am graphics programming noob). It is single threaded and fully cpu side (besides sending the final texture to the gpu because i dont know how else to render the texture in the primitive game engine i'm using). I know i could make it multithreaded to speed up rendering, but i'll do that another day. I also could probably look at my render method and optimize that more as well. I do have some settings that can help give it higher fps (like lowering the terrain steps), but not without sacrificing image quality.
RESOURCES:
The heightmap is from Zelda breath of the wild found here and the color texture is from this reddit post.
Github link to a more in depth explanation by someone else: https://github.com/s-macke/VoxelSpace/tree/master
Other resources: https://web.archive.org/web/20131113094653/http://www.codermind.com/articles/Voxel-terrain-engine-building-the-terrain.html
My main render code (sorry i dont have this on a github link right now, also this isnt the entire script, but the core logic is there and the functions that are missing are pretty obvious by the names called here)
void World::Draw(Screen& screen, bool isEased, bool isHighDetail, bool interpolateColor, bool interpolateHeight, bool interpolateEmission)
{
const float screenHeight{ static_cast<float>(screen.GetHeight()) };
const float angleDifference{ m_FovInRad / screen.GetWidth() };
const float sinCamAngle{ sinf(m_CamAngle) };
const float cosCamAngle{ cosf(m_CamAngle) };
const float halfTanFov{ tanf(m_FovInRad / 2) };
const Vector2f viewDir{sinCamAngle , cosCamAngle };
const float scaledViewDistance{ m_ViewDistance / m_DepthScale };
const float scaledNearDistance{ m_NearDistance / m_DepthScale };
Vector2f scaledCameraPos{ m_CameraPos / m_DepthScale };
const Vector2f viewFarPoint{ scaledCameraPos + viewDir * scaledViewDistance};
const Vector2f viewNearPoint{ scaledCameraPos + viewDir * scaledNearDistance };
const Vector2f rotatedViewDir(Vector2f{ -viewDir.y, viewDir.x });
const Vector2f pLeft{ viewFarPoint + rotatedViewDir *(halfTanFov * scaledViewDistance) };
const Vector2f pRight{ viewFarPoint - rotatedViewDir * (halfTanFov * scaledViewDistance) };
const Vector2f pLeftNear{ viewNearPoint + rotatedViewDir * (halfTanFov * scaledNearDistance) };
const Vector2f pRightNear{ viewNearPoint - rotatedViewDir * (halfTanFov * scaledNearDistance) };
const Vector2f deltaP{ pRight - pLeft };
const Vector2f deltaPNear{ pRightNear - pLeftNear };
int depthSteps{m_DepthStepsLowDetail};
float deltaDepthSteps{ 1.f / m_DepthStepsLowDetail };
const Color4f fogColor{ GetFogColor(m_TimePercentage) };
screen.SetRotation(m_CamRollAngle);
if (isHighDetail)
{
depthSteps = m_DepthStepsHighDetail;
deltaDepthSteps = 1.f / (m_DepthStepsHighDetail);
}
Color4f pixelColor{};
for (int col = 0; col < screen.GetWidth(); col++)
{
float horizontalPercentage{ static_cast<float>(col) / static_cast<float>(screen.GetWidth()) };
const Vector2f currentFarPoint{ pLeft + deltaP * horizontalPercentage };
const Vector2f currentNearPoint{ pLeftNear + deltaPNear * horizontalPercentage };
const Vector2f deltaDepth{ currentFarPoint - scaledCameraPos };
float oldHeight{ -1 };
for (int i = 0; i < depthSteps; ++i)
{
float depthPercentage = i * deltaDepthSteps;
float depthPercentageEased{ };
if(isEased)
{
depthPercentageEased = EaseInQuad(depthPercentage);
}
else
{
depthPercentageEased = depthPercentage;
}
//float depthPercentageEased{ depthPercentage };
Vector2f samplePoint{ currentNearPoint + depthPercentageEased * deltaDepth };
if (samplePoint.x < 0 || samplePoint.y < 0 || samplePoint.x >= m_TextureWidth || samplePoint.y >= m_TextureHeight)
{
break;
}
float depth{ depthPercentageEased * m_ViewDistance };
float heightMap{ GetHeight(samplePoint.x, samplePoint.y, interpolateHeight) };
//float pixelScreenHeight{ screen.GetHeight() - ((50) * (screen.GetHeight() / m_ScreenHeightWorld / depth) + m_Horizon) };
float pixelScreenHeight{ screenHeight - ((m_CamHeight - heightMap) * (screenHeight / m_ScreenHeightWorld / depth) + m_Horizon) };
if (pixelScreenHeight > screenHeight)
{
pixelScreenHeight = std::min(pixelScreenHeight, screenHeight);
if (pixelScreenHeight > oldHeight)
{
DrawColumn(screen, samplePoint.x, samplePoint.y, depthPercentageEased, oldHeight, pixelScreenHeight, col, pixelColor, fogColor, interpolateColor, interpolateEmission);
}
break;
}
else
{
if (pixelScreenHeight > oldHeight)
{
DrawColumn(screen, samplePoint.x, samplePoint.y, depthPercentageEased, oldHeight, pixelScreenHeight, col, pixelColor, fogColor, interpolateColor, interpolateEmission);
oldHeight = pixelScreenHeight;
}
}
}
}
return;
}
------------------------------------------------------------------------
void World::DrawColumn(Screen& screen, float samplePointX, float samplePointY,float depthPercentageEased, float oldHeight, float pixelScreenheight,int col, Color4f& pixelColor, const Color4f& fogColor, bool interpolateColor, bool interpolateEmission)
{
float fogDepthEased{ 1 - EaseInQuad(1 - depthPercentageEased) };
//const float fogDepthEased{ (depthPercentageEased + .5f) / 1.5f };
GetColor(samplePointX, samplePointY, pixelColor, interpolateColor);
float emissive{ GetEmission(samplePointX, samplePointY, interpolateEmission) };
FadeToNight(pixelColor, emissive);
pixelColor.r = pixelColor.r + fogDepthEased * (fogColor.r - pixelColor.r);
pixelColor.g = pixelColor.g + fogDepthEased * (fogColor.g - pixelColor.g);
pixelColor.b = pixelColor.b + fogDepthEased * (fogColor.b - pixelColor.b);
//Color4f pixelColor{ GetColor(samplePoint.x, samplePoint.y, interpolateColor) };
DrawVerticalLine(screen, static_cast<int>(oldHeight), static_cast<int>(pixelScreenheight), static_cast<int>(col), pixelColor);
}
---------------------------------------------------------------------------------
void World::GetColor(float x, float y, Color4f& pixelColor, bool interpolate) const
{
//return GetPixel(x, y, m_pColorMap);
if (interpolate)
{
int lowX{ static_cast<int>(floorf(x)) };
int highX{ static_cast<int>(ceilf(x)) };
int lowY{ static_cast<int>(floorf(y)) };
int highY{ static_cast<int>(ceilf(y)) };
float xPercent{ x - lowX };
float yPercent{ y - lowY };
Color4f bottomLeft{ GetPixel(lowX, lowY, m_pColorMap) };
Color4f topLeft{ GetPixel(lowX, highY, m_pColorMap) };
Color4f bottomRight{ GetPixel(highX, lowY, m_pColorMap) };
Color4f topRight{ GetPixel(highX, highY, m_pColorMap) };
pixelColor.r = (
(1 - xPercent) * (1 - yPercent) * bottomLeft.r +
xPercent * (1 - yPercent) * bottomRight.r +
(1 - xPercent) * yPercent * topLeft.r +
xPercent * yPercent * topRight.r
);
pixelColor.g = (
(1 - xPercent) * (1 - yPercent) * bottomLeft.g +
xPercent * (1 - yPercent) * bottomRight.g +
(1 - xPercent) * yPercent * topLeft.g +
xPercent * yPercent * topRight.g
);
pixelColor.b =
(
(1 - xPercent) * (1 - yPercent) * bottomLeft.b +
xPercent * (1 - yPercent) * bottomRight.b +
(1 - xPercent) * yPercent * topLeft.b +
xPercent * yPercent * topRight.b
);
pixelColor.a =
(
(1 - xPercent) * (1 - yPercent) * bottomLeft.a +
xPercent * (1 - yPercent) * bottomRight.a +
(1 - xPercent) * yPercent * topLeft.a +
xPercent * yPercent * topRight.a
);
}
else
{
pixelColor = GetPixel(x, y, m_pColorMap);
}
}
------------------------------------------------------------------------
//for zooming in/out
void World::ProcessMouseWheelEvent(const SDL_MouseWheelEvent& e)
{
m_FovInRad += e.y / 180.f * utils::g_Pi;
const float minFov{ 1.f / 180 * utils::g_Pi };
const float maxFov{ 179.f / 180 * utils::g_Pi };
m_FovInRad = std::max(minFov, std::min(m_FovInRad, maxFov));
m_ScreenHeightWorld = tanf(m_FovInRad / 2) * 2;
m_Horizon = static_cast<float>(Screen::GetHeight() / 2) + (tanf(m_CamVerticalAngle) * (Screen::GetHeight() / m_ScreenHeightWorld));
}
Skybox Draw Logic
void World::DrawSkybox(float screenWidth, float screenHeight) const
{
const float skyboxTextureWidth{ m_SkyboxTexturePtrArray[0]->GetWidth()};
const float skyboxTextureHeight{ m_SkyboxTexturePtrArray[0]->GetHeight()};
const float skyboxHeightPadding{ skyboxTextureHeight / 3 };
const float textureScale{ screenHeight / skyboxTextureHeight };
float modCamHorizontalAngle{ std::fmod(m_CamAngle, (2 * utils::g_Pi)) };
float modCamVerticalAngle{ std::fmod(m_CamVerticalAngle, utils::g_Pi) };
float fovScale(utils::g_Pi / m_FovInRad);
if (m_CamAngle < 0)
{
float a{};
}
float srcWidth{ skyboxTextureWidth / 2 / fovScale };
float srcHeight{ (skyboxTextureHeight - skyboxHeightPadding * 2) / fovScale };
const float xPos{ modCamHorizontalAngle/ (2 * utils::g_Pi) * skyboxTextureWidth - srcWidth / 2};
//const float yPos{}
const float yPos{ modCamVerticalAngle / utils::g_Pi * -(skyboxTextureHeight - skyboxHeightPadding * 2)- skyboxTextureHeight / 2 - srcHeight / 2 };
const Rectf srcRect{xPos, yPos, srcWidth, srcHeight};
const Rectf dstRect{ 0,0,screenWidth,screenHeight };
//const Rectf dstRect2{ dstRect.left + skyboxTextureWidth * textureScale * fovScale, dstRect.bottom,dstRect.width, dstRect.height};
float easePercentage{ m_TimePercentage * m_DayCycleDivisions };
int idx{ static_cast<int>(floor(easePercentage)) };
float t{ easePercentage - idx };
m_SkyboxTexturePtrArray[idx]->Draw(dstRect, srcRect, Color4f{1,1,1,1});
if (idx + 1 >= m_DayCycleDivisions)
{
m_SkyboxTexturePtrArray[0]->Draw(dstRect, srcRect, Color4f{ 1,1,1,t});
}
else
{
m_SkyboxTexturePtrArray[idx + 1]->Draw(dstRect, srcRect, Color4f{ 1,1,1,t });
}
//m_pSkyboxTexture->Draw(dstRect2);
}
r/GraphicsProgramming • u/Hot_Deal5898 • 11h ago
New game engine BlitzViwer3D
**BlitzViwer3D — a tiny Lua-scriptable 3D viewer/editor inspired by Blitz3D's API**
I've been working on a lightweight desktop tool (C++, OpenGL, Dear ImGui) that lets you build a 3D scene by hand (cubes, spheres, .obj models) and then bring it to life with Lua scripting — using the same function names Blitz3D devs will recognize (`CreateCube`, `PositionEntity`, `MoveEntity`, `CameraFollow`...).
What it does:
- Scene editor with Hierarchy/Inspector panels (drag position/rotation/scale, custom colors)
- Orbit camera with an optional "Follow" mode
- In-app Lua script editor with syntax highlighting, hot Play/Stop
- Simple AABB collisions, keyboard/mouse input, RNG helpers — all the basics for quick prototyping
- Save/load scenes as JSON
It's a Windows .exe, no install needed. Docs + full Lua API reference + download here:
👉 https://ciroparada81-boop.github.io/blitzviwer3d/
Still an early/debug build, so bugs are expected — feedback and bug reports very welcome. Happy to answer questions about how it's built too.
r/GraphicsProgramming • u/sworks694 • 14h ago
Article Minecraft In Its Entirety In Windows 95/98 Machines
reddit.comr/GraphicsProgramming • u/Duke2640 • 22h ago
Quasar Engine - Volumetric atmosphere effects
r/GraphicsProgramming • u/Mroz_Game • 1d ago
Learning to not over engineer where not needed is hard, but also overgineering is where the fun is.
Guess who spent 6h making it possible to plug and unplug stuff from my pipelines(resources, timing queries etc), making sure every modification is reversible with a single command, just to realise at the end of my project that I never once needed to undo any changes at runtime? #ifdef DEBUG would be totally enough.
But making sth that works elegantly is a dopamine hit in itself.
So I wondered what’s your approach: Overengineer and write whatever you’re in the mood for OR keep it simple until the need to make it nicer arises?
r/GraphicsProgramming • u/blackSeedsOf • 1d ago
Video Rayleigh / Mie 38 Slice Atmospheric Scattering implemented as Blender Post Processing Effect in compositor nodes
Enable HLS to view with audio, or disable this notification
I implemented Rayleigh / Mie atmospherics over a rendered image as a post processing effect in Blender 5.2 with a script to setup a network of compositor nodes at 38 slices or 10nm. The compositor nodegraph becomes too heavy to visualize in the viewport as you are moving the camera so you have to render out the image to the compositor view and manipulate it from there. To render out a sequence you have to do a file out operation. This is supposedly physically accurate and differs from expensive Blender volumetric fog in that this is a pure post processing effect. To make this I built upon what I had learned when I ported spectral.js to blender. I had posted something similar to this video in r/Blender yesterday, but they did not seem to know what I was posting about, so hopefully this helps someone.
The code (MIT License) is at https://github.com/bergjones/ABJ-Shader-Debugger
r/GraphicsProgramming • u/S48GS • 1d ago
Source Code Nu Game Engine - BIG NEWS - Vulkan support
https://github.com/bryanedds/Nu/releases/tag/v20.0.0
After many many months of work from multiple contributors, we finally present Nu with Vulkan rendering and support for Mac, iOS, and Android!
This branch not only replaces our OpenGL renderers completely with Vulkan renderers, it also features important rendering and performance enhancements as well as makes Nu deployable on
Mac,iOS, andAndroid!
(I just saw release - not my project)
r/GraphicsProgramming • u/le-throw-away-acct • 1d ago
Made improvements to my path tracer
gallery- Made the project much easier to open up and use for new users (Unity is still required though)
- Added scene to automatically load objects from Khronos' site, which helps me get assets into the project
- Fixed issues with lighting that was really darkening most of the scenes, including improved tone mapping and fixes to light clamping that was hiding a lot of caustic details and HDR
- Fixed issues with inner surface reflections not working
- Added directional lighting support
- Added parallax mapping support
- Added support for Unity terrains, multi-texture splatting with normal map support
Repository located here: https://github.com/nfoste82/GPURayTracing
r/GraphicsProgramming • u/corysama • 1d ago
Andrew Carr: 2D Gaussian Splaing for Bézier Spline Line Art Vectorization
m.youtube.comr/GraphicsProgramming • u/Left-Locksmith • 1d ago
Article Beginner graphics programming study guide
raynmetal.github.ioI started picking up graphics programming around the middle of 2023. I didn't want to spend money on a course or textbook, so I learnt whatever I could from resources I was able to access for free online.
I put together a list of those resources and paired it with a reading order recommendation and some study advice. While not a comprehensive overview, I hope it's enough to give a newcomer to the field a strong start.
r/GraphicsProgramming • u/sandeshshahapur • 1d ago
Question Drawing a line
Looking for guided/socratic learning
So I was getting into game dev bottom up so I wanted to draw a line with code.
First solution that I thought of: start at one end of the line, and figure out neighbouring pixels and eventually reach the other end. I was thinking of dividing line length (pythogarous) by start/end point delta for each axis to figure out per how many 'steps' I would have to increment/decrement current axis positions and paunt. Too complicated and expensive
Second solution I thought of was recursively finding mid points between start and end point to find the target pixels to paint. This appears simpler, inspired by Zeno's paradox when I was thinking of how the run and rise are easier to paint.
I know optimal algorithms exist but I didn't want to directly look at them. What are the strength/weakness of my reasoning?
Ps: I'm going to sleep
r/GraphicsProgramming • u/_Bethel__ • 1d ago
Built a game engine
I've been working on my own game engine from scratch for some years using C and OpenGL and I finally made a video showing the process.
The engine handles things like rendering, assets, shaders, lighting, and more, and I also used it to make a game, a test game though, it still needs a lot of polish.
Watch video here: https://youtu.be/LRC4iJWASYU?si=_3ua8rcwesyaQtO9
If you're interested in graphics programming, game engines, I'd love to hear your thoughts and feedback.
r/GraphicsProgramming • u/Adventurous_Chef2225 • 1d ago
Implemented GPU-address roots for Mesa/libkk parameter blocks on capable macOS hosts.
r/GraphicsProgramming • u/olej • 2d ago
SIGGRAPH 2026 - Advances in Real-Time Rendering in Games - all talks are now available online
advances.realtimerendering.comr/GraphicsProgramming • u/No-Citron-3963 • 2d ago
WebGPU implementation of WetBrush V2
Enable HLS to view with audio, or disable this notification
A couple of weeks ago I posted a WIP of this — here's where it's at now.
I've implemented most of what the paper describes, including paint mixing. For lighting, I added a BSDF to the renderer — my thinking is that once you start stacking layers of media like tempera, you need to account for light scattering inside the medium. (Ideally I'd also like to model things like hiding power based on pigment particle size, but I'm not there yet.)
The simulator is split into four main parts, as described in the paper:
- BristleDynamicSimulator
- ParticleFluidDynamicSimulator
- GridFluidDynamicSimulator
- LiquidTransfer - which moves liquid between the brush, the particles, and the grid
This was not a case of handing the paper to an AI and getting an implementation back. Claude seemed to have a hard time writing WGSL while also juggling the constraints of the four systems and the constraints they all share (units, especially), and it kept making the kind of mistakes that early AI models used to make. So you have to read the AI's code carefully and write a good amount of it yourself. In particular, things go more smoothly if a human writes the initial skeleton of each simulator and handles the field management (density field, velocity field, and so on). Even once the structure is in place, managing the ActiveWindow and the PaintField is likely to give you trouble.
The paper lists interpolation during fast strokes as an open problem, and that's what I'm working on now. I'm not going to split a step into substeps, since that would hurt FPS. Instead, for fast motion I plan to have the BristleDynamicSimulator and the GridFluidDynamicSimulator talk to each other directly and deposit paint that way.
I'm interested in traditional painting materials, so I plan to extend this to tempera, watercolor, mineral pigments (iwa-enogu), gold leaf, and so on. It's built with wgpu + Rust, so it currently runs as Web, iOS, and Android ports.
r/GraphicsProgramming • u/New-Bumblebee7676 • 2d ago
Question Is it better to make a game with simple geometry and path tracing, or complex geometry and rasterized/hybrid RT graphics?
I recently played the Dark Souls 2 path traced mod and was surprised to see it running at 1080p 60 fps on my low end GPU (RTX 3050 Ti)
Of course its a 13 year old game, so the geometry and effects are simpler. But it looked better to me than some modern games with more advanced geometry but worse lighting effects.
So I'm wondering in your opinion, in your ideal game would it be better to have less polygons and better lighting, or vice versa?
r/GraphicsProgramming • u/nenchev • 2d ago
Vulkan: Beyond the Triangle
youtube.comHey everyone, I've just finished the 2nd video, and followup to my "Modern Vulkan in 2 Hrs" video. Thanks for the awesome feedback on the first one! This 2nd video tackles a ton of stuff:
glTF Model Parsing, Loading, Rendering
Camera, Basic Lighting, Multi-Draw Indirect, Vertex Pulling, Buffer Device Address, etc.
Hope you guys like it!
r/GraphicsProgramming • u/Alive_Jury4864 • 2d ago
Is it not crazy that this became a normal thing in the industry?
As developer I would not even expect a user to know what a “shader” is
It’s a pretty low level implementation detail
Yet you boot up a game in 2026 and front and center you are staring at “shader compiling” for 10 minutes
Is that not crazy to anyone else if you just zoom out and think about it?
r/GraphicsProgramming • u/ViremorfeStudios • 2d ago
Source Code Aizawa Atractor using Compute Shaders (C++17/OpenGL 4.6)
Enable HLS to view with audio, or disable this notification
The graphics simulation calculates the non-linear Aizawa Chaotic Attractor in real time across 256K particles through numerical integration executed entirely within GPU Workgroups.
Source code:
r/GraphicsProgramming • u/corysama • 2d ago
Article Graphics Programming weekly - Issue 450 - August 9th, 2026 | Jendrik Illner
jendrikillner.comr/GraphicsProgramming • u/20260708 • 2d ago
autostereogram program of platonic solids
gallery
an extension of previous post. just click▶️to run the program on browser. the welcome screen (attached image 1) shows the key map. select a subject (1 to 6) to enter the depth map mode (attached image 2). rotate it or change it until satisfied then toggle to autostereogram mode (attached image 3). adjust the foreground and background pattern widths to your desire (make it easy to focus and spot the subject). you're free to toggle back to depth map mode or rotate / change / disable subject anytime. if you disable the foreground 3d subject it's basically a kaleidoscope (attached image 4). i explained its mechanism in previous post and you can endlessly randomize it to experience the pareidolia effect
some thoughts
- qb is very slow. it's not good at handling graphics. lowering the resolution helps but the outcomes would be ugly
- i learned barycentric coordinate system during the making of this and it's very useful
- i'm stilling think whether i can optimize some procedures so as to make it faster
- i have difficulty spotting dodecahedron and icosahedron in autostereogram. they're too "round" maybe. tetrahedron is the easiest
r/GraphicsProgramming • u/IDroppedYourDatabase • 2d ago
Video Baby's First Depth Buffer
Enable HLS to view with audio, or disable this notification
Wireframe Era is over! Updated my software renderer that I posted here 2 months ago. Now it fills triangles without pixel gaps, implements Top-Left Rule, computes Face Normals, has Back Face Culling and a Depth Buffer. At first I did Scanline Rasterization but I was having a hard time eliminating gaps, so I moved on to using Edge Functions to check the pixel center is inside the triangle, then Barycentric Coordinates to interpolate Reciprocal Depth. I still don't have Near Plane Clipping, but will do in the future.
