Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. Star us to help devs discover the project, give it a try, and share your feedback to help improve the product.
Miles Morales moonwalked 6.5 metres past the edge of a billboard.
Not on the billboard.
Past it.
On nothing.
I had watched that shot more than once and did not notice.
The viewport did not notice either.
A 44 line Python script noticed, and it has been catching my mistakes ever since.
This is part two of my accidental Blender education.
In part one I went from never having opened Blender to rigging a Spider-Man model, pulling animations from Mixamo and retargeting old Carnegie Mellon mocap onto him.
This one is about what happens next, when you try to turn that into an actual film and every mistake is suddenly a bug report.
The film, and why it is really a build system
The goal is a roughly 60 second explainer for LiveReview.
A masked Miles web-swings through a city at sunset, and every "slide" is part of the street.
A wall board, a rooftop billboard, an LED ticker, bus stop and subway lightboxes, a big street screen, and a runaway train for the finale.
Here is the twist that shapes everything below.
Nothing in the scene is hand edited.
The whole thing is generated headless by Python scripts against Blender 5.2, rendered with EEVEE on a 4 GB GTX 1650.
build/street.blend is not a file I open and tweak.
It is a build artifact, like a dist/ folder.
So every fix is a code change plus a rebuild, and the workflow ended up looking a lot more like CI than like art class:
flowchart LR
S[Python scripts] -->|blender -b| B[street.blend]
B --> C{support check clean?}
C -->|no| S
C -->|yes| D[--fast draft render]
D --> T[review copy with timecode]
T --> R{looks right?}
R -->|no, f1201 is off| S
R -->|yes| G[git commit]
G --> F[full quality render]
classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
classDef code fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
classDef build fill:#6ea8ff,stroke:#2f62c4,color:#1a1a1a
classDef render fill:#ff9a5c,stroke:#c4602a,color:#1a1a1a
classDef done fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
class C,R decision
class S,G code
class B build
class D,T render
class F done
Almost every lesson in this post fell out of that loop.
"Can't you just use the GPU to render faster?"
This was my first question once renders started taking a while.
The answer was a little embarrassing.
It already was.
EEVEE is a GPU rasterizer, basically a game engine renderer.
While it rendered, the card sat at about 80% utilisation and 2.2 GB of VRAM.
There was no idle GPU waiting to be switched on.
And the other renderer, Cycles, is a path tracer, which on a GTX 1650 would have been slower, not faster.
So the real question was never "CPU or GPU".
It was "what is each frame paying for?"
Three settings turned out to dominate:
| Setting | What it costs | Draft value |
|---|---|---|
| Screen-space ray tracing | reflections on the wet street, the single biggest per-frame cost | off |
| Motion blur | renders extra time steps for every frame | off |
| Samples | anti-aliasing and soft shadows, linear in render time | 8 instead of 16 to 24 |
So the render script grew a --fast flag that flips exactly those three:
fast = "--fast" in args # draft: no ray tracing, no motion blur, 8 samples
...
if fast:
sc.eevee.taa_render_samples = samples or 8
if hasattr(sc.eevee, "use_raytracing"):
sc.eevee.use_raytracing = False
sc.render.use_motion_blur = False
At 360p that took a frame from about 0.6 s to about 0.33 s.
Roughly 2x.
The full 58 second film is 1,760 frames, so a review render went from around 18 minutes to around 10.
What you give up is real but harmless.
The street looks flatter without the ray-traced reflections, and fast motion looks crisper without blur.
Nobody reviewing "does Miles land on the ledge" cares about either.
The finals still get everything.
A few other levers came along for the ride:
- A 270p option (480x270) for the quickest "is the blocking right" passes.
- Contact sheets. A strip of stills at 25% resolution and 8 samples, before committing to any video at all.
-
Partial re-renders.
05_render.py -- full 33 <from_frame>re-renders only from a given frame onward and reuses the earlier frames, for when only the ending changed.
The lesson is the same one you learn profiling a slow API.
Before you ask for a bigger machine, find out what the work actually is.
Most of the time you are paying for something nobody looks at.
"How do I go back? Is there a snapshot button?"
Blender has more undo than you would expect, in layers:
- Ctrl+Z and Ctrl+Shift+Z for in-session undo and redo, plus Edit → Undo History to jump back several steps at once.
- File → Revert to throw away unsaved changes and reload from disk.
-
.blend1backups. Every save keeps the previous version next to the file, and Preferences → Save & Load → Save Versions sets how many (docs). - File → Recover → Auto Save for when Blender crashes, which it will.
All useful, and all beside the point here.
Because when the .blend is generated, it is not the source of truth.
The scripts are.
So the real snapshot button is git:
git log --oneline -- miles/scripts/04_street.py # pick a commit
git stash # park current work
git checkout <commit> -- miles/ # that version's scripts and assets
blender -b build/character.blend --python scripts/04_street.py # rebuild that scene
git checkout HEAD -- miles/ && git stash pop # and come back
If the scene is generated from code, version the code, not the binary.
I commit after every review round now, so any version I ever showed someone can be rebuilt exactly.
The old rendered videos stick around too (street_v1, v4a and friends), because sometimes you just want to play two versions side by side without rebuilding anything.
A reader comment that found a real bug
Part one got a comment from @lewisywliu that was genuinely better than some docs I have read.
Two tips, paraphrased:
BVH files define their own euler rotation order per joint, and the importer does not always match it, so wrists and feet can end up subtly wrong even when the hips look fine. If a pose reads almost right but the hands are off, check euler order before touching offsets.
Consecutive clips with different hip heights, like land to idle or crouch to stand, need the vertical offset blended too, otherwise the character floats or sinks for a few frames while the horizontal path snaps correctly.
The first one turned out to be a near miss.
The second one was a bug I already had and had not seen.
Tip 1: euler order, or why the hands go wrong first
A BVH file, which is what the CMU mocap library ships as, stores each joint's rotation as three angles.
But it also stores the order those three rotations are applied in, and that order is per joint.
One joint can say Z then X then Y, the next can say Y then X then Z.
That matters because rotations do not commute.
Rotate 30 degrees around Z and then 45 around X, and you end up facing somewhere different from 45 around X and then 30 around Z.
Same numbers, different pose.
Now put that in a skeleton.
Every bone inherits its parent's rotation, so a small error at the shoulder moves the elbow, which moves the wrist, which moves the hand.
The error compounds down the chain.
The hips are at the top of the tree, so they look perfect.
The hands are at the bottom, so they are the first thing that looks "almost right but weird".
I checked the importer call, half expecting the worst.
It already passed rotate_mode="NATIVE" to bpy.ops.import_anim.bvh, which means "use each joint's own order from the file".
So we were safe.
But it is now the first thing on my list if a CMU clip ever shows odd wrists or ankles, before anyone reaches for an offset hack.
Tip 2: hip height across a blend, which bit me
Quick refresher on how the clips are chained.
Each clip is mostly "in place", so to make Miles actually travel, each clip's movement is added on top of wherever the last one ended.
A running sum.
That is root motion, the same idea game engines use.
Everyone remembers to blend the horizontal part of that path.
The second tip was that the vertical part needs blending too.
(Game engines usually call that Y. Blender is Z-up, because of course it is.)
On flat ground I never saw a problem.
On raised surfaces I had exactly this bug, and I only found it once I measured it.
The probe was simple: on every frame through each blend, take the gap between the lowest toe and the surface under him.
The target is about +5 cm, because the toe bone sits a little above the sole of the shoe.
| Blend, on a raised surface | Before | After |
|---|---|---|
| Crouch → web shot, on a board's top ledge | sinks ~10 cm for 3 frames | +0.03 to +0.07 m |
| Crouch → moonwalk, on a billboard catwalk | pops up 22 cm, then settles | +0.02 to +0.10 m |
| Crouch → taunt, on a rooftop | drifts up 10 cm | +0.04 to +0.07 m |
A 22 cm pop is a Spider-Man who briefly levitates for no reason.
Which, to be fair, is on brand, but not what I wanted.
The first fix did exactly what the tip says, and it did nothing.
I eased the root height smoothly across the blend window instead of letting it step.
The numbers barely moved.
The measurement showed why.
The hips were not the problem.
The crossfaded pose was.
Halfway between a crouch and a standing pose, the blended legs are half extended, so the feet rise on their own, no matter how gently the hips move.
A smooth curve for the hip height cannot follow what the legs are doing.
What worked was to stop predicting and start measuring.
For every frame in a raised shot, find the lowest toe, and move the root so that toe sits a fixed distance above the surface.
Then smooth it lightly so it does not jitter.
This is the real function, trimmed a little:
def contact(self, start, end, floor, toe=0.05, smooth=2, limit=0.35):
"""Keep the lowest toe `toe` above a surface at height `floor` for every frame."""
frames = list(range(int(start), int(end) + 1))
fc = anim.fcurve(self.root, "location", 2)
base, low = [], []
for f in frames:
self.scene.frame_set(f)
base.append(fc.evaluate(f) if fc else self.root.location.z)
low.append(min((self.rig.matrix_world @ self.rig.pose.bones[b].head).z
for b in ("mixamorig:LeftToeBase", "mixamorig:RightToeBase")))
corr = [max(-limit, min(limit, floor + toe - z)) for z in low] # a hop in the clip stays a hop
sm = [sum(corr[max(0, i - smooth):i + smooth + 1]) / len(corr[max(0, i - smooth):i + smooth + 1])
for i in range(len(corr))] # average over ±2 frames
if fc: # remove back to front: indices stay valid
idx = [i for i, kp in enumerate(fc.keyframe_points) if start <= kp.co[0] <= end]
for i in reversed(idx):
fc.keyframe_points.remove(fc.keyframe_points[i])
for f, b, c in zip(frames, base, sm):
anim.key(self.root, "location", f, b + c, 2, "lin") # a key on EVERY frame
That limit=0.35 is load bearing.
Without a cap, one clip that genuinely hops at the end got "corrected" by 8 metres.
The function saw feet in the air, decided the floor must be way off, and dragged the whole character to meet them.
The ±35 cm clamp means a real hop stays a hop.
And yes, that "remove back to front" comment is a scar.
Which brings us to the bugs.
Three bugs the measurement dug up
Once there was a probe printing a number per frame, the number started pointing at things I would never have seen in a viewport.
1. Miles, 18 metres up, for exactly one frame
An earlier floor-lock pass keyed the root height on every other frame, with linear interpolation in between.
That is a perfectly reasonable thing to do inside one shot.
It is a terrible thing to do across a hard cut.
At a cut from a rooftop to the street, the frame between two keys landed halfway between two shots.
Linear interpolation averaged "rooftop" and "street" and put Miles about 18 m up, in the sky above the street, for one frame.
At 30 fps that is a 33 millisecond glitch.
You would never catch it playing the video.
The probe flagged it instantly.
The fix: key every frame inside these ranges, so there is no in-between frame left to interpolate.
2. Hard cuts on fractional frames
Clips are scheduled in float frames, because blend lengths and speeds are computed.
So a "hard cut" could start at frame 1037.4.
Blender renders whole frames, which meant frame 1037 showed the old pose at the new position.
Half of one shot glued to half of another.
The fix: hard cuts snap to a whole frame with math.ceil, and a shot's last frame is ceil(end) - 1.
This is the animation version of an off-by-one, and it was hiding inside a float.
3. Deleting keyframes while iterating over them
This one is a classic, and I still walked straight into it.
The code collected references to the keyframes it wanted to replace, then looped over them calling keyframe_points.remove(kp, fast=True).
But removing a keyframe shifts every index after it.
So the next reference in the list now pointed at its neighbour, and the loop silently deleted the wrong key.
The first frame of two shots ended up with no height key at all.
It is the same bug as removing items from a Python list inside for x in items:, just wearing a 3D costume.
The fix is the one in the snippet above: collect indices, then remove from the back.
The whole lesson of this section is not about any one of those bugs.
It is that the first fix looked right in theory and did nothing in practice, and only a number per frame could tell me that.
Do not eyeball root motion. Measure it.
The probe became a test suite
Once the foot probe existed, it seemed silly to only run it when I suspected a problem.
So it became scripts/check_support.py, a permanent pre-render check.
It is about 40 lines and it answers one question, sampling every third frame of the film: is there something under his feet?
flowchart TD
F[Next frame] --> A{airborne clip or upside down?}
A -->|yes, swing, hang, flip| F
A -->|no| T[Find the lowest toe]
T --> R[Ray cast straight down]
R --> M{hit Miles, a web, a spider or cash?}
M -->|yes, step past it| R
M -->|no| G{gap over 25 cm or sinking?}
G -->|no| F
G -->|yes| B[Flag the frame]
B --> F
classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
classDef step fill:#6ea8ff,stroke:#2f62c4,color:#1a1a1a
classDef bad fill:#ff9a5c,stroke:#c4602a,color:#1a1a1a
classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
class A,M,G decision
class T,R step
class B bad
class F start
It uses Blender's scene.ray_cast, ignores anything that is part of the character or the props (a web is not a floor, sadly), skips the moments where he is supposed to be in the air, and groups flagged frames into runs so the output reads like a test report.
I run it before every render, the same way you would not deploy without running the tests.
And it caught things a visual check flat out missed.
Moonwalking on thin air.
The opening of this post.
Miles moonwalked 6.5 m past the edge of a billboard's frame, and, for bonus points, in the opposite direction to the customers walking off the screen behind him.
The fix had two parts.
A steel catwalk under the screen, because real billboards have one for maintenance anyway.
And the moonwalk now goes with the customers, which is a better joke.
A 40 cm stance on a 25 cm board.
He stood on top of a wall board whose top was 25 cm deep.
His stance was 40 cm.
One foot was hanging off the front, standing on air.
The fix was a 1.25 m service ledge on top of the board.
A turn that swung him 2 m off a ledge.
This was my favourite, because the cause was pure geometry.
In-place clips leave the rig's root pivot nowhere near the hips.
In this case it was about 4 m away.
So when a blend rotated the root to turn him 150 degrees, his whole body orbited a point 4 m away, like a door swinging on a hinge in the next room.
Mid-turn, his hips were 2 m off the ledge.
The fix was to key the root per frame during a turning blend, so the hips travel in a straight line while the body rotates around them.
A turn is a rotation about some point.
It is worth checking which one.
Small workflow wins
None of these deserve a section, but all of them saved real time.
Burned-in review copies.
Every render also writes a <name>_tc.mp4 with the Blender frame number and "time / total" stamped in the corner.
When someone says "f1201 looks off", that maps straight to a frame in the scene.
No more "uh, around the middle, after the bus thing".
Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.
I'm building LiveReview, a blast-radius aware AI code review built for your business-critical systems.
Instead of presenting every diff with equal emphasis, LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.
Spend code review effort where business risk is highest — not spread evenly across every diff.
⭐ Star it on GitHub:
HexmosTech
/
LiveReview
Blast-Radius Aware AI Code Review for Business-Critical Systems
LiveReview: Blast-Radius Aware AI Code Review for Business-Critical Systems
LiveReview is an AI code reviewer that scores every hunk of a diff by blast radius: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.
blast-radius-demo.mp4
LiveReview's Blast Radius & Review Priority scoring, live in the diff viewer.
Here's the goal:
- A 3-line fix in a function used by 40 other files, that also writes to a database, should score high.
- A 300-line UI change in one file, fully covered by…
Click below to try LiveReview with your codebase:













Top comments (0)