Vignesh K.
← Projects
Personal R&D · 2026 · Realtime 3D

WrapX

Live, interactive retopology. Load a clean base mesh and a scan, click matching points on each, press Play, and watch the template flow onto the scan — then grab it and push it around while it is still solving.

The stills below are frames from one solve. The video is the part they cannot show.

  • C++ · no Python in the loop
  • In-process solver thread
  • Quads preserved
  • OBJ + FBX
  • Reversed-Z GL + Skia
  • GPL-3.0

Wrapping a base mesh onto a scan is normally a batch step. You place landmarks, submit, wait, and inspect what comes back; if it is wrong you adjust the landmarks and submit again. WrapX is the argument that the interesting part is the ten seconds you are not allowed to see — so the solve runs in-process, at a watchable pace, in the viewport you are already looking at.

There is no batch step, no export round trip and no Python in the loop. Press Play and the template starts crawling onto the target; press Grab and drag, and the mesh stays soft under the cursor and keeps solving while you pull. The Python pipeline this was ported from still exists — as the reference implementation the C++ is tested against, not as a runtime dependency.

Step 116 — settle

01

Step 116 — settle

Two seconds in. The template is still wearing the scan like a loose coat, and the magenta underneath is the part it has not reached yet. Surface error 1.075% of model size, 20% of snap attempts rejected. Nothing has been written to disk and nothing was exported to get here.

Step 341 — coarse

02

Step 341 — coarse

The anneal schedule has moved on and the magenta has mostly closed up, surviving at the hands and feet where thin features sit close together. 0.462%, 14% rejected. You can pause here, or grab it and pull, and it keeps solving under your cursor.

Step 1500 — holding at detail

03

Step 1500 — holding at detail

The end of the schedule, about ten watchable seconds later: 0.138% surface error, 7% rejected, quads intact. It holds at detail rather than stopping, so a late grab still settles instead of freezing mid-edit.

The fit, checked outside the tool

Point clouds, three views
Non-rigid fit shown as point clouds from the front, the side, and the head in profile
After the non-rigid fit. The template’s vertices (red) sit on the scan (grey) from the front, from the side, and in profile at the head, including the ears, nose and lips, where thin features sit closest together.
Base mesh in grey against the fitted morph in red, from the front, the side, and the head in profile
Back in the rest pose, as a morph. Grey is the untouched base and red is the fitted shape: shorter arms, different head proportions, the same vertex order. Because the topology never changes, the result works as a morph target on the original mesh.

What makes it different

The solve is the interface

An earlier version of this wrote both meshes and the point list to disk and spawned a Python pipeline as a subprocess. It produced good numbers and the wrong product: you pressed a button, waited, and looked at an answer. Now the solver runs in this process on its own thread, at a pace you can watch, and you can interrupt it whenever you like.

Grab it while it runs

A grab has to suppress snapping or it does nothing — at the final stage the data term pulls with about 0.857 per step, so a grabbed vertex is yanked back onto the scan before you see it move. The grab writes its falloff into the snap rejection mask and fades out over ~15 steps, and the anneal clock freezes for the duration so the mesh does not stiffen the longer you hold it.

Fluid because of the integrator

Grabs move positions without touching the Verlet history, so they inject velocity rather than teleporting. That is why the mesh keeps flowing for a moment after you let go. The soft feel is a consequence of how the solver integrates, not an effect layered on top of a finished result.

Quads survive the round trip

The mesh type stores polygons, not triangles, and the wireframe uses a per-triangle edge mask so a quad draws four edges instead of five. Most loaders triangulate on import, which would destroy the one property a retopology tool exists to maintain — which is also why the OBJ parser is hand-written and FBX goes through ufbx rather than Assimp.

How a wrap runs

Three stages, none of them a wait

Point pairs

Click a point on the template, then the matching point on the target. Landmarks are stored as a polygon plus barycentric coordinates, so a set is reusable across any model with matching topology and two nearby clicks never collide. Symmetry mirrors each new pair across X.

Play

Structural springs, Verlet integration, Laplacian smoothing and surface snapping, driven by a real annealing schedule on a worker thread. The anneal bar, the step counter and the live surface error are all painted from the running solve.

Grab

Push the wrap around while it settles. Shift-drag pulls along the surface normal, Ctrl+wheel sizes the brush. Grabbing keeps the solver stepping even when paused — otherwise a pull would just translate a frozen shape with no springs to carry it outward.

Measuring the right thing

Quality vector, not surface RMS

Surface RMS alone is minimised by a mesh collapsed onto the target, and it cannot see a single one of the failures that actually cost you work. So the wrap reports a vector: folds, self-intersections, edge distortion and landmark residual alongside surface error.

That is what makes the snapping trade legible. On anything with thin close-together features — fingers, lips, eyelids — default SURFACE snapping takes the nearest point unconditionally, so opposing surfaces a millimetre apart grab each other. Measured on a full body:

Snap modeFoldsSelf-intersecting pairsSurface error
SURFACE (default) 21 200+ 0.19%
OUTSIDE 5 49 0.61%

Three times the surface error, for a quarter of the folds. Topological damage is the more expensive kind, because it breaks UVs and everything downstream of them.

Two tools, one shell

The WrapX Studio launcher

WrapX is the wrap tool. FlowX, alongside it and still in development, comes at the same problem from the other end: remesh a sculpt or scan into clean quads from a cross field, and comb that field with strokes to steer the edge flow where you want it.

Both sit on a shared shell, and the geometry, the maths and the whole solver live in libraries that link neither GL nor the UI toolkit — which is what keeps 26,194 test assertions running without a window.

Things that were not obvious

Picking on a mesh that is moving

Ray queries against a deforming mesh refit the BVH rather than rebuilding it — one backward sweep over the existing tree. The split planes go stale as the mesh moves so the tree slowly loses efficiency, but it stays correct, because a query only descends into boxes that still contain their triangles.

One GL block per frame, both viewports

Skia's Ganesh backend records draws and replays them at the final submit, while raw GL executes immediately. Rendering each viewport inside its own paint would cost a flush and a full state reset per viewport, and would let a shared texture display the other viewport's contents.

The test that keeps earning its place

A projection round trip: project a vertex to screen coordinates, cast a ray back through that pixel, assert it lands on the same point — under both depth conventions, on an offset non-square viewport. Skia draws y-down in logical pixels; GL renders y-up in device pixels. It has caught the ray being built backwards, and an unprojection outside the reversed-Z frustum.

Release, or it stutters

The solver runs about 3.5x faster in Release — 1.2 ms per step against 4.2 ms at 4,776 vertices. That is the entire difference between a live wrap that holds 60 fps while you drag it and one that visibly stutters, so Release is the default in the build script rather than a flag you remember.

Alpha, and honest about it. It wraps, live, and every number on this page is measured rather than estimated. But the solver is at an early phase of a planned five: the shear, bending and curvature-preserving terms are not in yet, so expect a slightly soft fit and some quad drift. A T-pose template onto an A-pose target still fails badly — a single global thin-plate spline cannot express a large limb rotation. One file is a port of GPL-3.0 code, which makes the binary GPL-3.0; it is deliberately the only one.

Ask me about it → vignesh0702@gmail.com