1 Million Particles Rendering at 120 FPS
▲ 26 r/rustgame+6 crossposts

1 Million Particles Rendering at 120 FPS

https://reddit.com/link/1v4m9dw/video/gbips9icv0fh1/player

I've been hacking away on my own game engine for a while. Decided to benchmark it today, and the results were better than I expected. The engine is sitting around 8.1–8.9 ms per frame in the steady-state tests, which comes out to roughly 110–122 FPS. That's while keeping frame times pretty stable.

Right now it's simulating 1 million particles with 4 XPBD iterations. The physics pass itself finishes in about 4.8–5.0 ms on the GPU, so there is still some room left in the frame.

https://preview.redd.it/pfcsqjf7v0fh1.png?width=1600&format=png&auto=webp&s=4e8678614e691f0ec318daf1dd1ff96b8e9fc5a0

https://preview.redd.it/5vonhq49v0fh1.png?width=1600&format=png&auto=webp&s=0f931f0a3c8ed66a14d0aacce5977cafa8459b8a

https://preview.redd.it/kq0amr49v0fh1.png?width=1600&format=png&auto=webp&s=5de78d1ab60a28cfb861a4119c9d4766ab7601b1

The initial upload moves around 68 MB of particle data into VRAM. That takes about 91 ms over PCIe. Since it's only a startup cost, I'm not too worried about it.

One thing I kept an eye on was the telemetry. It never reported the frame going over budget. The [PERF_OK] Frame within budget message stayed consistent for the whole run, and I didn't see any frame-time spikes.

Still a lot of things I want to optimize, but I'm pretty happy with where it's at right now.

reddit.com
u/IamRustyRust — 2 days ago
▲ 1 r/rust_p

1M Particles Rendering

SO I am still working on this engine in Rust was stuck here for a while although I am still not satisfied but yeah I made some progress.

Non-SGP

SGP

reddit.com
u/IamRustyRust — 1 month ago
▲ 0 r/rust_p

How a GPU memory pipeline handles data streams and why it fundamentally bottlenecks.

What if insetad of Hi-Z pyrmaid if use 1D Array if I think that Hi-Z iteration take some overheead so in that case what if I use 1D Array.

https://preview.redd.it/c2ak98k8z74h1.jpg?width=964&format=pjpg&auto=webp&s=ab82d16c814d796eb1f253593f278af7b014d4fe

Everything starts with how the CPU organizes our game world. The CPU sets up the memory mapping using Page Table Entries. To the CPU, this is highly structured data. It knows exactly how objects are organized and linked together. But the GPU? It doesn't see it that way. When the GPU needs to read the data stream buffer, it uses its own Translation Lookaside Buffer (TLB) the GPU TLBto figure out where that data physically lives in the memory chips.

And this is where the hardware disconnect hits hard. When the GPU tries to pull this data into its L1 and L2 caches, it completely loses that structure the CPU built. To the GPU's memory fetcher, this data is just a tramp of raw bits. No spatial awareness at all. Because the cache doesn't know what data is grouped together physically in the 3D scene, it cannot smartly predict what to load next. The direct physical result? We get a cache miss.

When the data isn't sitting right there in the L1 or L2 cache, the hardware has to go all the way out to the physical VRAM to fetch it. That takes a massive delay of roughly 600 clock cycles.

>So, why does this matter? Because during those 600 cycles, the actual math computing units on the GPU the ALUs have absolutely zero data to process. They are just sitting there doing nothing. This is ALU starvation. And here is the most frustrating part. After the ALUs literally starve for 600 cycles waiting for raw bits, they finally run the math. But 90% of that data gets rejected anyway because of culling. The ALU realizes the triangles are behind a wall and throws the data in the trash. We starved our hardware for 600 cycles for data we didn't even use.

https://preview.redd.it/fmknr6mwz74h1.jpg?width=964&format=pjpg&auto=webp&s=aa9fa532df2e6980b5945e3c2000a2a500779123

That entire cycle of waiting and rejecting is terrible, but it gets even worse when we look at how the actual shape of the data completely chokes the cache.

Let's start from step zero. When we feed data to the GPU, if I will use 1D array (depends entirely on how the programmer actually writes the game engine code), this is an O(N) operation. The system just steps through the list one item at a time. But here's the major problem. This linear data is entirely non-spatial. It has absolutely no idea what is actually sitting next to what in the physical 3D game world. Because of this, the hardware takes the fast whip path. It just whips through the data blindly. This causes temporal blindness. The GPU's cache cannot smartly predict what data it needs next because the list order doesn't match the 3D space on your screen.

For small objects, this blind approach is actually fine. The tiny data just sits in the cache. But it completely falls apart for big objects. Let's say we check if a massive object is hidden with a basic if (object > pixel depth) condition. To compute that, we have to physically bring that pixel depth data all the way from memory directly into the ALU registers. Big objects require a heavy quantity of data. That data travels from VRAM down into the tiny L1 and L2 caches, and it just can't fit. The caches get overwhelmed and flush out the good data. Another cache miss. We end up right back at ALU starvation, doing zero math.

So, stuffing heavy 1D data into tiny caches ruins performance, which means we desperately need a way to filter this garbage out before it hits the pipeline.

We fix this with a Hi-Z Pyramid. What exactly is that? The GPU tracks how far away things are using a depth buffer. The Hi-Z Pyramid takes that full-resolution depth data and mathematically scales it down into smaller and smaller chunks, creating mip levels. So, why does this matter? Because for a massive object, the GPU doesn't check thousands of individual pixels anymore. It just checks the lowest resolution mip level. The depth check instantly drops down to an O(1) operation.

>But there is a huge problem. We literally cannot inject the visible meshlets directly into the Graphics Pipeline. The physical hardware won't let you loop that data back in cleanly. Step 1 is the GPU running the math against our Hi-Z Pyramid. If the math proves the meshlet is visible, the hardware sets a predicate patch to 1. If it's hidden, it sets it to 0. Just raw hardware flags. But now we have a massive mess of 1s and 0s randomly scattered all over memory. If we feed that to the pipeline, we get those exact same cache misses. So, Step 2 is the Subgroup Ballot. This is a bare-metal hardware instruction. A block of GPU threads basically votes on their predicate flags. The ballot instruction physically sweeps through the threads, ignores the 0s, collects all the 1s, and packs them into a perfectly dense, continuous stream.

https://preview.redd.it/ftzp6xktz74h1.jpg?width=964&format=pjpg&auto=webp&s=b131965d9a39a18020bdf7444effed3e18326fa6

So, stuffing heavy 1D data into tiny caches ruins performance, which means we desperately need a way to filter this garbage out before it hits the pipeline.

We fix this with a Hi-Z Pyramid. But what exactly is that? The GPU tracks how far away things are using a depth buffer. The Hi-Z Pyramid takes that full-resolution depth data and mathematically scales it down into smaller and smaller chunks, creating mip levels. So, why does this matter? Because for a massive object, the GPU doesn't check thousands of individual pixels anymore. It just checks the lowest resolution mip level. The depth check instantly drops down to an O(1) operation.

But there is a huge problem. We literally cannot inject the visible meshlets directly into the Graphics Pipeline. The physical hardware won't let you loop that data back in cleanly.

  • Step 1 is the GPU running the math against our Hi-Z Pyramid. If the math proves the meshlet is visible, the hardware sets a predicate patch to 1. If it's hidden, it sets it to 0. Just raw hardware flags. But now we have a massive mess of 1s and 0s randomly scattered all over memory. If we feed that to the pipeline, we get those exact same cache misses.
  • Step 2 is the Subgroup Ballot. This is a bare-metal hardware instruction. A block of GPU threads basically votes on their predicate flags. The ballot instruction physically sweeps through the threads, ignores the 0s, collects all the 1s, and packs them into a perfectly dense, continuous stream.

>
Now that we have this dense stream of surviving data, we run into a new problem: how do we write it to memory without stalling the cores again?

https://preview.redd.it/158kjxzxz74h1.jpg?width=964&format=pjpg&auto=webp&s=79f136bdbdc789ccb1ce131967204fee57b52b80

We finally have our alive meshlets. We need to write them directly into the L1 Cache. If we use a standard linear loop to check each of the 32 threads in our GPU warp, it takes exactly 32 clock cycles. The processing cores completely stall out. ALU starvation again. To fix this, we look at the 32-bit hardware register holding those collected 1s and 0s. The hardware does a raw flip of that data directly into a 32 predicate patch feeding the 32 ALUs. This happens right inside the registers. A single clock cycle. The whole warp instantly knows exactly which threads are alive, with absolutely zero memory latency.

>But we still need to physically route and pack that scattered data tightly into memory. This is where the Banyan tree Kogge-Stone Parallel scan comes in. (Banyan-Switch)

It's just a mechanical way the hardware adds numbers together. Instead of adding item one to item two, then waiting to add item three, the circuitry adds multiple pairs at the exact same time. It builds a tree structure to calculate the final memory offsets. This exact operation is called a Parallel Prefix Sum. Instead of taking standard linear time, it drops the routing math down to a logarithmic scale, written mathematically as O(log N).

https://preview.redd.it/q3d6ofg8084h1.jpg?width=964&format=pjpg&auto=webp&s=e50749e6666fcb34c4c82403a7e211d02933d321

So, let's look at the top part first where I wrote down that I use SubgroupExclusiveAdd. Why do we actually need this? Well, if we are rendering something massive, I might have 4 billion particles on the screen. If every single particle needs to figure out its unique place in a list, and we just use a basic Index - 1 loop to count them down, that math operation is going to physically run 4 billion times. It completely stalls the hardware. But we can prevent that. SubgroupExclusiveAdd is a raw hardware instruction where a block of GPU threads basically talks to each other and calculates all their index offsets simultaneously in one single step. It completely eliminates that slow, linear counting process.

And that leads perfectly into the second half of the page, which I called The VRAM Bypass. Usually, when threads finish processing their data, they dump it all the way out into the main VRAM. But doing that is incredibly slow because the next part of the graphics pipeline has to fetch that data all the way back again. So, we just skip it entirely. The threads write their data directly into the L1 shared memory. This is tiny, ultra-fast memory that physically sits right inside the processing cores.

>But how do we actually keep the data there without the GPU flushing it? I wrote down a dispatcher command called EmitMeshTasksEXT. This acts as a direct hardware trigger. As soon as the data fetch happens straight from the L1 memory, this command fires off.

>But here is where I deliberately oversimplified the drawing so it wouldn't become a visually complex mess. In the diagram, I drew the next batch of work the mesh shaders spawning exactly on those same physical cores. And in a perfect world, if that specific core has free execution slots, that is exactly what the hardware scheduler does. The data literally never leaves the L1 cache.

But the GPU is a dynamic machine. What if that specific core is already 100% full? The hardware scheduler will not just sit there and wait. It instantly grabs that new mesh shader work and throws it to another free core. Because L1 cache is strictly private to each individual core, the hardware automatically pushes our payload data out of L1 and into the L2 cache, which is shared across the whole GPU. The new core just fetches the raw bits from L2 instead.

We still completely bypassed that massive, ultra-slow trip to VRAM, but we let the hardware dynamically balance the load.

If you want to see these exact bare-metal hardware concepts (Hi-Z occlusion, Subgroup thread packing, and bypassing VRAM entirely) running in real-time, here are some incredible developer deep-dives that perfectly match the hardware bottlenecks I drew above:

reddit.com
u/IamRustyRust — 2 months ago

Rendering 1 Million Procedural Cubes

https://reddit.com/link/1tav6k1/video/upp9zug11o0h1/player

I’ve been working on my engine (Rust obviosly), specifically on Vulkan bindings. The main work was already done, and only testing remained. During testing, I usually prefer CSV and JSON because they give me a good grasp of the data, letting me easily see what’s happening and spot any unexpected behavior. This saves a lot of time since you don’t have to check every number individually you just need to confirm whether things are going as expected. Since continuous testing was already happening, I knew that for this stage I only needed an overall overview to ensure all components were working together properly, as individual component testing had been done earlier. So yesterday, I was testing, Today, I decided to share CSV graphs and visual testing results.

Here’s my result:

Workload: 1,000,000 procedurally generated cubes (8 million vertices / 12 million primitives).

Average frametime: ~1.43ms (consistently hitting 700+ FPS).

PCIe bandwidth used: exactly 0 bytes.

1% lows: extremely stable (max spikes under 2.5ms).

// Flushing data to disk without pollutng hotloop!!!!!!
if state_arc.lock().unwrap().mode == 0 {
    if let Ok(mut f) = std::fs::File::create("alu_throughput_log.csv") {
        let _ = writeln!(f, "Timestamp_Sec,Cubes_Generated_Per_Frame,Vertices_Computed_By_ALU,Triangles_Rasterized,Memory_Bandwidth_Used_Bytes");
        for t in &alu_log {
            let _ = writeln!(f, "{:.4},{},{},{},{}", t.timestamp_sec, t.cubes_generated, t.vertices_computed, t.triangles_rasterized, t.memory_bandwidth);
        }
    }
    
    if let Ok(mut f) = std::fs::File::create("frame_pacing_log.csv") {
        let _ = writeln!(f, "Frame_ID,Timestamp_Sec,Render_Latency_ms,Instant_FPS");
        for t in &pacing_log {
            let _ = writeln!(f, "{},{:.4},{:.2},{:.0}", t.frame_id, t.timestamp_sec, t.render_latency_ms, t.instant_fps);
        }
    }

    if let Ok(mut f) = std::fs::File::create("dispatch_consistency_log.csv") {
        let _ = writeln!(f, "Frame_Window_Start,Frame_Window_End,Avg_Latency_ms,Max_Latency_Spike_ms_1_Percent_Low");
        for t in &consistency_log {
            let _ = writeln!(f, "{},{},{:.2},{:.2}", t.window_start, t.window_end, t.avg_latency_ms, t.max_latency_spike_ms);
        }
    }
}

If you look at the cluster of dots in the graph, you’ll clearly see that despite the heavy render load, most frames are densely clustered around 1.5ms latency and a median of 661 FPS.

The system is processing 5 to 9 B triangles per second. (the right axis of the graph.) Render latency is consistently maintained between 1.5ms and 2.5ms (The solid dotted green line )

reddit.com
u/IamRustyRust — 3 months ago
▲ 25 r/renderings+5 crossposts

Rendering 1 Million Procedural Cubes

https://reddit.com/link/1tav653/video/upp9zug11o0h1/player

I’ve been working on my engine (Rust obviosly), specifically on Vulkan bindings. The main work was already done, and only testing remained. During testing, I usually prefer CSV and JSON because they give me a good grasp of the data, letting me easily see what’s happening and spot any unexpected behavior. This saves a lot of time since you don’t have to check every number individually you just need to confirm whether things are going as expected. Since continuous testing was already happening, I knew that for this stage I only needed an overall overview to ensure all components were working together properly, as individual component testing had been done earlier. So yesterday, I was testing, Today, I decided to share CSV graphs and visual testing results.

Here’s my result:

Workload: 1,000,000 procedurally generated cubes (8 million vertices / 12 million primitives).

Average frametime: ~1.43ms (consistently hitting 700+ FPS).

PCIe bandwidth used: exactly 0 bytes.

1% lows: extremely stable (max spikes under 2.5ms).

// Flushing data to disk without pollutng hotloop!!!!!!
if state_arc.lock().unwrap().mode == 0 {
    if let Ok(mut f) = std::fs::File::create("alu_throughput_log.csv") {
        let _ = writeln!(f, "Timestamp_Sec,Cubes_Generated_Per_Frame,Vertices_Computed_By_ALU,Triangles_Rasterized,Memory_Bandwidth_Used_Bytes");
        for t in &alu_log {
            let _ = writeln!(f, "{:.4},{},{},{},{}", t.timestamp_sec, t.cubes_generated, t.vertices_computed, t.triangles_rasterized, t.memory_bandwidth);
        }
    }
    
    if let Ok(mut f) = std::fs::File::create("frame_pacing_log.csv") {
        let _ = writeln!(f, "Frame_ID,Timestamp_Sec,Render_Latency_ms,Instant_FPS");
        for t in &pacing_log {
            let _ = writeln!(f, "{},{:.4},{:.2},{:.0}", t.frame_id, t.timestamp_sec, t.render_latency_ms, t.instant_fps);
        }
    }

    if let Ok(mut f) = std::fs::File::create("dispatch_consistency_log.csv") {
        let _ = writeln!(f, "Frame_Window_Start,Frame_Window_End,Avg_Latency_ms,Max_Latency_Spike_ms_1_Percent_Low");
        for t in &consistency_log {
            let _ = writeln!(f, "{},{},{:.2},{:.2}", t.window_start, t.window_end, t.avg_latency_ms, t.max_latency_spike_ms);
        }
    }
}

If you look at the cluster of dots in the graph, you’ll clearly see that despite the heavy render load, most frames are densely clustered around 1.5ms latency and a median of 661 FPS.

The system is processing 5 to 9 B triangles per second. (the right axis of the graph.) Render latency is consistently maintained between 1.5ms and 2.5ms (The solid dotted green line )

reddit.com
u/IamRustyRust — 2 months ago

We already got a tiny taste of quaternions, but that was just the surface we Have seen the the imaginary numbers and we talked about how Xi + Yj + Zk really works but this represents a point on a 3D graph (P = Xi + Yj + Zk). Now we’re getting into the real deal the Rotation the actual Rotation.

Usually People call these 3D quaternions but calling them 3D quaternions is wrong. Quaternions are 4D engines built to spin things in 3D space. That’s why we use the full W + Xi + Yj + Zk setup.

Our big focus now is W.

You’ve got to understand that W sets the angle of the spin, but it has no physical direction. Why? Because direction needs an axes (X, Y, or Z), but W is a pure scalar. It’s just a raw number like 1, 2, or -1. You can plot it on a 1D line, but it doesn't point anywhere in your 3D room. It’s not a vector. It’s part of the quaternion, but it doesn't represent the axis you’re rotating around. It’s just the numerical weight.

>W is like a football referee. He is the most important guy on the field because he controls the rules and the clock, but he isn’t a player. He doesn't kick the ball or play for a team. W is that referee. It stays outside the 3D play (the axes), but it decides exactly how much the axes (X, Y, Z) are allowed to move.

We will see the role of W later in our Rotation later it was just an intro for now. It's time to our School math which we have seen in the part 1 too.

https://preview.redd.it/dv7kw7sek6xg1.jpg?width=963&format=pjpg&auto=webp&s=07af1465ca2caf2ad04a13e8c87949db6a497cd7

>We previously discussed flipping. Flipping is just taking a point on the X axis and giving it a 180 degree turn. Now it is -X. Mathematically, you are doing X . -1 = -X. That move to the opposite side of an axis is what we call flipping.

That -1 at the end in The School Math image is actually the cancellation of rotation. Why? Let us use a wall to explain this. Suppose your friend is behind a wall. You are on the the other side. To see this, imagine the X axis as the straight line on the floor that represents the path between you and your friend and the Y axis as the vertical line of the wall itself.

https://preview.redd.it/zrv0z991l6xg1.jpg?width=1280&format=pjpg&auto=webp&s=9c2e508f406e1623a678efd1274a4d4ea3f0cb13

>When you calculate X . i, your point moves off the floor and onto the wall. Since the floor and the wall sit at 90 degrees to each other, there is now a 90 degree angle. And if you multiply it again (X . i . i)? Now you are at your friend. Why? Because i . i is 180 degrees.

But what if the Y axis disappears? Only X is left. Can you make a 90 degree angle in that physical space? No. Physical space needs at least two dimensions (a 2D plane) for an angle to even exist. But here is the trick: When we talk about 3D space, for any rotation to physically occur, a 2D plane (like Y and Z) is needed because an angle simply cannot be formed without a plane. But when we talk about quaternion math, the quaternion does not need all three axes within itself. If you only want to rotate around the X-axis, then only 'i' will appear in the quaternion formula (like W + Xi). This formula does not need 'j' and 'k' to survive and do its job.

>While the physical space needs a plane to rotate, the quaternion itself only needs to define ONE single axis. That single axis acts as the invisible pole that spins the 2D plane. A quaternion functions perfectly and survives with just one axis.

The School math is telling us something here. When two identical imaginary numbers multiply together (i . i), the spatial rotation stops. You get a scalar. No axis means no direction, and no direction means there is no active spatial rotation.

>The quaternion doesn't just vanish. It just stops spinning. It flattens out into a pure number with zero direction. And that is exactly how a rotation ends. Because when we talk about imaginary numbers, we are really just talking about direction. And you can't have a direction without an axis. Axis gone? Direction gone. The moment that axis collapses into a pure scalar, the rotation is dead. So we can see how the rotation ended. When we talk about imaginary numbers, we are really talking about direction.

>

Hamilton's fundamental formula

>Look at this part carefully: i . j . k = -1. You just moved through all three axes one after the other. And the result is just a flat -1. But what does that -1 actually mean in physical space? In quaternion W = Cos(theta / 2). If your W hits -1 the math is telling you that the half-angle is exactly 180 degrees. Multiply that by 2 to get your real angle. You get 360 degrees. So it does not matter if you multiply the same two axes together (i . i = -1) or if you chain all three together (i . j . k = -1). Hitting that -1 scalar means your system just did a complete 360 degree spin. The object is sitting exactly where it started. This single fact is the absolute foundation the entire quaternion system is built on.

https://preview.redd.it/y7cu9tuql6xg1.jpg?width=1280&format=pjpg&auto=webp&s=e9f237c506fe01bbfdebc0f9f1d336a64d74afa8

>The system needs a raw value whose square is exactly 0.5. That specific value is 0.707 because 0.707 . 0.707 = 0.5. So the value for Cos(45) is 0.707 and the value for Sin(45) is also 0.707. Here no extreme 0 or 1 is formed. Both values are perfectly equal.

This proves W is a normalized number. Normalized means the total system capacity is always restricted to a strict length of 1. When W is 1 the rotation is 0 and the axes have 0 energy. When W is 0 the rotation is 180 and the axes hold all the energy.

>Why are we actually dividing the theta angle by 2? The physical rotation is 90 degrees but we feed 45 degrees into the Cos and Sin functions.

The answer lies in the mathematical sandwich approach known as q . v . q********^(-1).

>q applies half the rotation (theta/2) and q^(-1) applies the other half (theta/2) from the opposite side to keep the vector in 3D space. Without multiplying from both sides, the 3D vector gets pulled into 4D quaternion space.

To rotate a vector we must multiply it by a quaternion from the left and its inverse from the right. This structure applies the rotation effect twice. If we want a final physical rotation of theta we must use theta / 2 on the left and theta / 2 on the right so they add up perfectly to the full target angle.

What Happens to Scalar:

We have learnt Scalar means the axis is completely gone and it adds in W. But how does this newly formed scalar actually make that jump, and what exactly causes W to grow?

When you multiply two terms together like 2i and 3i you end up with 6(i . i). Since we already know that i . i is -1, that entire chunk just becomes -6. Just like that, the 'i' axis is wiped out completely.

You are left with a flat -6, which is a pure scalar. Because it lost its directional axis, it mathematically cannot exist in the X, Y, or Z positions anymore. Basic algebra dictates that like terms have to group up. So, that -6 has nowhere else to go. It is forced to shift over and add itself directly to your existing W value. And that is the exact mechanical way W absorbs these numbers.

https://preview.redd.it/0m8r3tjgl6xg1.jpg?width=1280&format=pjpg&auto=webp&s=feab6f83de2db5bcd5adffb2bd470455c10d859b

>What happens if all three axes (i, j, k) become scalars during a complex calculation? W accumulates the sum in a single numerical flow.
W = W + (2i . 3i) + (4j . 5j) + (6k . 7k) = W - 6 - 20 - 42 = W - 68

Understanding the Role of W using Law of Energy Conservation:

>W is a normalized number. Normalized means the total system capacity is always restricted to a strict length of 1. When W is 1 the rotation is 0 and the axes have 0 Rotation. When W is 0 the rotation is 180 and the axes hold all the Rotation.

We will understand the role of W in rotation through a concept similar to the law of conservation of energy. We will only look at values from 0 to 1 for W in this context. If W is 1 it means there is zero rotation and the object is at its exact starting place. As the rotation begins on the physical axes X to Y and Y to Z the value of W starts decreasing. We know that a full face turn to the back means the object rotated 180 degrees. At this exact 180 degree angle all the energy from W is drained out and W becomes 0.

>This proves the total energy in the system is fixed and it simply transfers from W to the physical axes. The mathematical equation W^(2) + X^(2) + Y^(2) + Z^(2) = 1 must always be fulfilled. Energy is neither created nor destroyed but it simply moves between the scalar W and the vector axes. This is why the value of W travels precisely between 0 and 1 during rotation. If we take that same object and rotate it in reverse by 180 degrees back to 0 degrees it will return to its exact starting position. All the rotational energy transfers back from the 3D axes to W making W equal to 1 again.

Now we will use Cos(theta/2) and Sin(theta/2). These trigonometric functions map exactly to our energy law. The Cos(theta/2) function calculates the value of W which represents the pure quaternion math rotation. The Sin(theta/2) function calculates the distribution of that energy across the physical 3D axes. The physical 3D world axes and the quaternion W exist in completely different mathematical spaces. This is why the 3D graph angle is not the exact same value as the internal quaternion math angle.

>You will see a complementary relationship between Cos(theta/2) and Sin(theta/2). Suppose the physical rotation angle is 180 degrees. The formula divides this by 2 so we get 180 / 2 = 90. Now we calculate Cos(90) and Sin(90). Both functions take the same 90 degree input but their outputs are exactly opposite in a complementary way. The value of Cos(90) is 0 and the value of Sin(90) is 1. Since Cos calculates W and Sin calculates the axes this physically means at 180 degrees of rotation W drops completely to 0 and the axes hold all the energy as 1. The mathematical rule 0********^(2) + 1********^(2) = 1 is perfectly maintained.

But because theta is divided by 2 there will also be a situation where Cos and Sin have exactly equal parts. Let us look at a real world angle of 90 degrees. The formula divides this by 2 which gives 45 degrees. We now need the limit for Cos(45) and Sin(45). If you trace this on a graph 45 degrees is the exact midpoint where the two lines cross. Because it is the exact halfway point between 0 (stationary) and 180 (fully rotated) degrees the math values must be split equally between W and the axes. Remember our rule W********^(2) + Axes********^(2) = 1. If W and the Axes must be exactly equal we need a number that gives 0.5 when multiplied by itself. We need 0.5 + 0.5 = 1.

reddit.com
u/IamRustyRust — 3 months ago
▲ 5 r/rust_p

Let's Understand Quaternions- Part 2

We already got a tiny taste of quaternions, but that was just the surface we Have seen the the imaginary numbers and we talked about how Xi + Yj + Zk really works but this represents a point on a 3D graph (P = Xi + Yj + Zk). Now we’re getting into the real deal the Rotation the actual Rotation.

Usually People call these 3D quaternions but calling them 3D quaternions is wrong. Quaternions are 4D engines built to spin things in 3D space. That’s why we use the full W + Xi + Yj + Zk setup.

Our big focus now is W.

You’ve got to understand that W sets the angle of the spin, but it has no physical direction. Why? Because direction needs an axes (X, Y, or Z), but W is a pure scalar. It’s just a raw number like 1, 2, or -1. You can plot it on a 1D line, but it doesn't point anywhere in your 3D room. It’s not a vector. It’s part of the quaternion, but it doesn't represent the axis you’re rotating around. It’s just the numerical weight.

>W is like a football referee. He is the most important guy on the field because he controls the rules and the clock, but he isn’t a player. He doesn't kick the ball or play for a team. W is that referee. It stays outside the 3D play (the axes), but it decides exactly how much the axes (X, Y, Z) are allowed to move.

We will see the role of W later in our Rotation later it was just an intro for now. It's time to our School math which we have seen in the part 1 too.

https://preview.redd.it/dv7kw7sek6xg1.jpg?width=963&format=pjpg&auto=webp&s=07af1465ca2caf2ad04a13e8c87949db6a497cd7

>We previously discussed flipping. Flipping is just taking a point on the X axis and giving it a 180 degree turn. Now it is -X. Mathematically, you are doing X . -1 = -X. That move to the opposite side of an axis is what we call flipping.

That -1 at the end in The School Math image is actually the cancellation of rotation. Why? Let us use a wall to explain this. Suppose your friend is behind a wall. You are on the the other side. To see this, imagine the X axis as the straight line on the floor that represents the path between you and your friend and the Y axis as the vertical line of the wall itself.

https://preview.redd.it/zrv0z991l6xg1.jpg?width=1280&format=pjpg&auto=webp&s=9c2e508f406e1623a678efd1274a4d4ea3f0cb13

>When you calculate X . i, your point moves off the floor and onto the wall. Since the floor and the wall sit at 90 degrees to each other, there is now a 90 degree angle. And if you multiply it again (X . i . i)? Now you are at your friend. Why? Because i . i is 180 degrees.

But what if the Y axis disappears? Only X is left. Can you make a 90 degree angle in that physical space? No. Physical space needs at least two dimensions (a 2D plane) for an angle to even exist. But here is the trick: When we talk about 3D space, for any rotation to physically occur, a 2D plane (like Y and Z) is needed because an angle simply cannot be formed without a plane. But when we talk about quaternion math, the quaternion does not need all three axes within itself. If you only want to rotate around the X-axis, then only 'i' will appear in the quaternion formula (like W + Xi). This formula does not need 'j' and 'k' to survive and do its job.

>While the physical space needs a plane to rotate, the quaternion itself only needs to define ONE single axis. That single axis acts as the invisible pole that spins the 2D plane. A quaternion functions perfectly and survives with just one axis.

The School math is telling us something here. When two identical imaginary numbers multiply together (i . i), the spatial rotation stops. You get a scalar. No axis means no direction, and no direction means there is no active spatial rotation.

>The quaternion doesn't just vanish. It just stops spinning. It flattens out into a pure number with zero direction. And that is exactly how a rotation ends. Because when we talk about imaginary numbers, we are really just talking about direction. And you can't have a direction without an axis. Axis gone? Direction gone. The moment that axis collapses into a pure scalar, the rotation is dead. So we can see how the rotation ended. When we talk about imaginary numbers, we are really talking about direction.

Hamilton's fundamental formula

>Look at this part carefully: i . j . k = -1. You just moved through all three axes one after the other. And the result is just a flat -1. But what does that -1 actually mean in physical space? In quaternion W = Cos(theta / 2). If your W hits -1 the math is telling you that the half-angle is exactly 180 degrees. Multiply that by 2 to get your real angle. You get 360 degrees. So it does not matter if you multiply the same two axes together (i . i = -1) or if you chain all three together (i . j . k = -1). Hitting that -1 scalar means your system just did a complete 360 degree spin. The object is sitting exactly where it started. This single fact is the absolute foundation the entire quaternion system is built on.

What Happens to Scalar:

We have learnt Scalar means the axis is completely gone and it adds in W. But how does this newly formed scalar actually make that jump, and what exactly causes W to grow?

When you multiply two terms together like 2i and 3i you end up with 6(i . i). Since we already know that i . i is -1, that entire chunk just becomes -6. Just like that, the 'i' axis is wiped out completely.

You are left with a flat -6, which is a pure scalar. Because it lost its directional axis, it mathematically cannot exist in the X, Y, or Z positions anymore. Basic algebra dictates that like terms have to group up. So, that -6 has nowhere else to go. It is forced to shift over and add itself directly to your existing W value. And that is the exact mechanical way W absorbs these numbers.

https://preview.redd.it/0m8r3tjgl6xg1.jpg?width=1280&format=pjpg&auto=webp&s=feab6f83de2db5bcd5adffb2bd470455c10d859b

>What happens if all three axes (i, j, k) become scalars during a complex calculation? W accumulates the sum in a single numerical flow.
W = W + (2i . 3i) + (4j . 5j) + (6k . 7k) = W - 6 - 20 - 42 = W - 68

Understanding the Role of W using Law of Energy Conservation:

>W is a normalized number. Normalized means the total system capacity is always restricted to a strict length of 1. When W is 1 the rotation is 0 and the axes have 0 Rotation. When W is 0 the rotation is 180 and the axes hold all the Rotation.

We will understand the role of W in rotation through a concept similar to the law of conservation of energy. We will only look at values from 0 to 1 for W in this context. If W is 1 it means there is zero rotation and the object is at its exact starting place. As the rotation begins on the physical axes X to Y and Y to Z the value of W starts decreasing. We know that a full face turn to the back means the object rotated 180 degrees. At this exact 180 degree angle all the energy from W is drained out and W becomes 0.

>This proves the total energy in the system is fixed and it simply transfers from W to the physical axes. The mathematical equation W^(2) + X^(2) + Y^(2) + Z^(2) = 1 must always be fulfilled. Energy is neither created nor destroyed but it simply moves between the scalar W and the vector axes. This is why the value of W travels precisely between 0 and 1 during rotation. If we take that same object and rotate it in reverse by 180 degrees back to 0 degrees it will return to its exact starting position. All the rotational energy transfers back from the 3D axes to W making W equal to 1 again.

Now we will use Cos(theta/2) and Sin(theta/2). These trigonometric functions map exactly to our energy law. The Cos(theta/2) function calculates the value of W which represents the pure quaternion math rotation. The Sin(theta/2) function calculates the distribution of that energy across the physical 3D axes. The physical 3D world axes and the quaternion W exist in completely different mathematical spaces. This is why the 3D graph angle is not the exact same value as the internal quaternion math angle.

>You will see a complementary relationship between Cos(theta/2) and Sin(theta/2). Suppose the physical rotation angle is 180 degrees. The formula divides this by 2 so we get 180 / 2 = 90. Now we calculate Cos(90) and Sin(90). Both functions take the same 90 degree input but their outputs are exactly opposite in a complementary way. The value of Cos(90) is 0 and the value of Sin(90) is 1. Since Cos calculates W and Sin calculates the axes this physically means at 180 degrees of rotation W drops completely to 0 and the axes hold all the energy as 1. The mathematical rule 0********^(2) + 1********^(2) = 1 is perfectly maintained.

But because theta is divided by 2 there will also be a situation where Cos and Sin have exactly equal parts. Let us look at a real world angle of 90 degrees. The formula divides this by 2 which gives 45 degrees. We now need the limit for Cos(45) and Sin(45). If you trace this on a graph 45 degrees is the exact midpoint where the two lines cross. Because it is the exact halfway point between 0 (stationary) and 180 (fully rotated) degrees the math values must be split equally between W and the axes. Remember our rule W********^(2) + Axes********^(2) = 1. If W and the Axes must be exactly equal we need a number that gives 0.5 when multiplied by itself. We need 0.5 + 0.5 = 1.

https://preview.redd.it/y7cu9tuql6xg1.jpg?width=1280&format=pjpg&auto=webp&s=e9f237c506fe01bbfdebc0f9f1d336a64d74afa8

>The system needs a raw value whose square is exactly 0.5. That specific value is 0.707 because 0.707 . 0.707 = 0.5. So the value for Cos(45) is 0.707 and the value for Sin(45) is also 0.707. Here no extreme 0 or 1 is formed. Both values are perfectly equal.

This proves W is a normalized number. Normalized means the total system capacity is always restricted to a strict length of 1. When W is 1 the rotation is 0 and the axes have 0 energy. When W is 0 the rotation is 180 and the axes hold all the energy.

>Why are we actually dividing the theta angle by 2? The physical rotation is 90 degrees but we feed 45 degrees into the Cos and Sin functions.

The answer lies in the mathematical sandwich approach known as q . v . q********^(-1).

>q applies half the rotation (theta/2) and q^(-1) applies the other half (theta/2) from the opposite side to keep the vector in 3D space. Without multiplying from both sides, the 3D vector gets pulled into 4D quaternion space.

To rotate a vector we must multiply it by a quaternion from the left and its inverse from the right. This structure applies the rotation effect twice. If we want a final physical rotation of theta we must use theta / 2 on the left and theta / 2 on the right so they add up perfectly to the full target angle.

reddit.com
u/IamRustyRust — 3 months ago