#1328 Snapshot: avatars only, transparent background (phase 1) #1341

Closed
TheSlowly wants to merge 9 commits from TheSlowly/Slipstream:snapshot-avatars-1328 into main
Collaborator

Part of #1328 (phase 1). The existing "Avatar (Transparent)" snapshot layer becomes "Avatars only (transparent background)":

  • Soft edges: alpha is carried through the HDR chain as coverage, averaged over the supersampled frames and unpremultiplied in linear HDR. Hair and lace keep soft edges, with no 50% cut and no dark fringe. The PNG has straight alpha, and resizing is done premultiplied.
  • What cuts avatars: world objects in front still do, decided by depth on the GPU. Avatars left out of the shot stay in the world: they still cast shadows and reflections, and act as walls.
  • Scope: "Selected" can leave you out. Animesh is included only under All.
  • RLVa: the layer is refused under a vision sphere, with a notification.
  • Normal frames are unchanged: the regular tonemap's SPIR-V is byte-identical.

A fresh review was done and its findings are fixed. Tested in-world.

Possible follow-ups, not in this PR: studio lighting, a shadow catcher, saving to SL, and other avatars' flexi attachments.

Part of #1328 (phase 1). The existing "Avatar (Transparent)" snapshot layer becomes **"Avatars only (transparent background)"**: - **Soft edges:** alpha is carried through the HDR chain as coverage, averaged over the supersampled frames and unpremultiplied in linear HDR. Hair and lace keep soft edges, with no 50% cut and no dark fringe. The PNG has straight alpha, and resizing is done premultiplied. - **What cuts avatars:** world objects in front still do, decided by depth on the GPU. Avatars left out of the shot stay in the world: they still cast shadows and reflections, and act as walls. - **Scope:** "Selected" can leave you out. Animesh is included only under All. - **RLVa:** the layer is refused under a vision sphere, with a notification. - **Normal frames are unchanged:** the regular tonemap's SPIR-V is byte-identical. A fresh review was done and its findings are fixed. Tested in-world. Possible follow-ups, not in this PR: studio lighting, a shadow catcher, saving to SL, and other avatars' flexi attachments.
The avatar scope's middle option was "Self + Selected": your own avatar was
always in, so a cut-out of a friend (or a group shot without you behind the
camera) was not possible. It is now "Selected": the docked picker lists you
first, ticked to begin with, and unticking yourself leaves you out. Select All
ticks you too, Clear unticks you.

Free-standing animesh (pets, NPCs) are only taken in by "All"; an animesh worn
by someone still counts as its wearer. The rule is a small viewer-free unit
(slsnapshotscope.h) with tut tests.

Left out, your avatar leaves the offscreen render's main view the way "Show me
in Mouselook" off does it (LLVKWorldFrame::hide_self): its pre-skinned draws
also serve the shadow and mirror passes, so it cannot leave every pass as the
other unchosen avatars do, and its shadow stays in a colour snapshot.

Test-Plan: Snapshot floater, Capture: Colors, interface off, "Avatars:
  Selected": the list beside the window shows you first, ticked. Untick
  yourself and refresh: you are gone from the preview (your shadow stays);
  tick yourself again: you are back. Clear unticks you, Select All ticks you.
  With a free-standing animesh pet in view: it is in the shot with "Avatars:
  All" only, not with "Selected" or "Self only". An animesh you wear stays
  with you in every scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A transparent avatar snapshot that renders smaller than requested (the device's
image limit, video or system memory, #730) or comes out larger than the save
limit is resized after the render. LLImageRaw::scale averaged the straight RGBA
bytes, so the black of the transparent pixels bled into every partly covered
pixel of the result: a dark rim around the cut-out on any light background.

The layer now resamples premultiplied colour and divides by the resampled
alpha afterwards (slstraightalpha.h, viewer-free with tut tests). Alpha 0 is
the floor: transparent black, no stray colour where nothing is.

Test-Plan: Snapshot floater, Capture: the transparent avatar layer, Save to
  disk at a size above what the card renders (the floater notes it was
  reduced, e.g. 8192 x 8192 on a small card). Open the PNG over a white
  background: the avatar's edges and hair show no dark outline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The layer's alpha was not opacity. Each avatar-only recording read its
avatar-id pass and depth back to the host and the CPU counted covered pixels;
alpha-blended hair or lace counted as covered at 50 % texel alpha or more, so a
soft edge became a hard cut, or the hair's blend over black kept full alpha and
showed as a dark fringe. The colour was then unpremultiplied after the tonemap,
in sRGB bytes, which is only an approximation.

The alpha now travels with the colour, as coverage in the HDR scene's A:
- After the opaque scene and the composites over it (camera medium, volumetric
  fog, precipitation veil), a compute clears every pixel with no avatar surface
  or with the world in front of it, and sets A = 1 on the rest
  (clean_snap_holdout.comp). Fog and haze on an avatar therefore stay; what
  they painted over the background goes.
- The world occluder comes from the warm-up recording on the GPU: its depth
  wherever its avatar-id pass shows none of the layer's avatars. A fullscreen
  depth draw merges it into the depth before the transparent layers, so each
  hair or lace fragment behind the world fails its own depth test. A soft
  fringe in front of a wall stays soft.
- The alpha pipelines already composite A "over" and the supersampling already
  averages RGBA, so the scene ends as premultiplied colour plus antialiased
  coverage. A tonemap variant (TONEMAP_STRAIGHT_ALPHA) unpremultiplies in
  linear HDR before the exposure and the tonemapper, writes straight colour
  and A, and renderOffscreen reads RGBA back. Alpha below half an 8-bit step
  is the floor: transparent black. That also bounds the division (x510 at
  most). The mattes are this alpha.

The HDR alpha had another meaning: the medium noFade bit, written by the
deferred lighting and consumed by clean_medium_apply.comp, which runs before
the holdout. Its other readers in these recordings are the snapshot
accumulation and the new tonemap, plus the motion blur, which is now off here.

Nothing in the layer carries coverage for light in the air or over the
background, so these are off in its avatar-only recordings: god rays, falling
precipitation, the snow surface mesh in the main view (it still casts),
effects (HDR and display), CAS / RCAS sharpening, motion blur. Bloom, glow,
lens effects and FXAA / SMAA already were. The alpha-depth pre-pass is also
off: the holdout reads the depth after pass 1 as the avatars' opaque surfaces.
An RLVa vision sphere still applies; its pass writes opaque pixels, so the
layer then has no alpha (logged once).

Normal frames are untouched: every change is gated on an avatar-only
recording, and the normal tonemap's SPIR-V is byte-identical. All the work is
on the GPU: one compute after the warm-up, then one compute and one depth draw
per recording. The id pass now runs in the warm-up only. A 1080p shot with 16
antialias frames no longer reads about 200 MB of ids and depth back or loops
over them on the CPU, so preview refreshes get cheaper. The occluder costs
4 bytes per pixel of video memory for the shot, allocated softly: without it,
walls do not cut avatars (logged).

Test-Plan: Snapshot floater, Capture: the transparent avatar layer, Save to
  disk (PNG); antialias on (default). Open it over a checkerboard or a bright
  colour in an image editor:
  - long alpha-blended hair: soft, partly transparent strands, no hard cut and
    no dark outline; sheer or lace clothing is see-through;
  - stand behind a table or half behind a wall: the hidden part is cut, the
    hair in front of the wall keeps its soft edge;
  - glowing attachments, bloom on: no halo around the avatar in the PNG;
  - with fog or heavy haze: the avatar keeps its tint, the background is fully
    transparent; in snow, no ground snow and no flakes in the PNG;
  - the two matte layers match the PNG's alpha.
  A colour snapshot of the same view looks as before. With antialias off the
  layer still has alpha (aliased edges).
Test-Settings: SnapshotAntialias = 1, RenderSnapshotTemporalFrames = 16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scope used to take unchosen avatars out of every pass of the layer's
render, warm-up included. Their shadows on the chosen avatars went with them,
and so did their reflections. A chosen avatar standing behind an unchosen one
also showed through it whole.

In an avatar layer, the avatars outside the scope (or not shown by the view
with the world) are now treated as world. Only the main view of the layer's
avatar-only recordings leaves them out:
- computeAvatarVis files them as off-screen avatars in those recordings, so
  the legacy cull keeps their rigged draws for the sun cascades, projector
  tiles, mirror and realtime probe (and the RT), and drops them and their worn
  attachments from the main view;
- the cluster selection gets the main view's avatar bits (ClSelectAux
  only_main, read under CLV_AVATARS_ONLY): their cluster-drawn meshes are still
  selected for the rig lists and the RT but not drawn there, as CLV_HIDE_SELF
  does with your own;
- you, when "Selected" leaves you out, are hidden through mHideSelf in those
  recordings only (LLVKWorldFrame::avatar_hide_self);
- the warm-up still renders everyone. The occluder's per-slot bits now come
  from the scope, so the warm-up's depth of a left-out avatar cuts the layer's
  avatars behind it like a wall.
World snapshots with a scope keep the old behaviour: unchosen avatars leave
every pass, so a group shot has no stray shadows.

Test-Plan: Two avatars A (you) and B side by side in sunlight, B's shadow
  falling on you. Snapshot floater, Capture: the transparent avatar layer,
  "Avatars: Selected" with only you ticked: the PNG shows you with B's shadow
  on you, and no B. Move so that B stands in front of you: the part of you
  behind B is cut out. Untick yourself, tick B: the PNG shows B only, with your
  shadow on B where it falls. Also try a free-standing animesh pet: it is in
  the layer with "All" only, and casts on you otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Avatar (Transparent)" read as one avatar, see-through. The layer is the
avatars in view as the scope picks them, cut out with soft edges on a
transparent background. The value (avatar_transparent) and the settings are
unchanged.

Test-Plan: Snapshot floater, Capture list: the entry reads "Avatars only
  (transparent background)" (in full in the open list) and saves the same
  transparent PNG the old entry did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After each of the layer's avatar-only recordings, recordSSRCopy copied its
scene and depth over SSR's previous frame. That scene is the avatars over
transparent black, and its depth carries the world occluder merged in by the
holdout. From the second jittered recording on, every reflection ray toward the
world hit a black occluder pixel instead of missing and falling back to the
probes: with antialiasing, glossy outfits came out about 15/16 darker in their
reflections.

In those recordings nothing is copied now and the previous view and projection
stay the warm-up's, so every recording traces the normal view (world included).
The scene and depth still move to the layouts the copy would have left them in.

Test-Plan: RenderScreenSpaceReflections on, antialias on. Wear a shiny (high
  gloss) outfit near a bright wall and take the transparent avatar layer: the
  reflections on the outfit are as bright as in a colour snapshot of the same
  view, not dark.
Test-Settings: RenderScreenSpaceReflections = 1, SnapshotAntialias = 1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The occluder took the warm-up's depth wherever its avatar-id pass showed none
of the layer's avatars. That pass draws the legacy rigged buckets only, and
with the cluster LOD path owning rigged meshes (the default) those avatars
have no ids. A chosen cluster-drawn avatar's own depth then became its
occluder: see-through seams along its internal silhouettes (an arm over the
torso, jittered), and the body behind its own hair cut out. Such avatars were
also missing from the seen set, so the layer dropped them altogether. The id
pass also counted alpha-blended hair at 50 % texel alpha as avatar, which
lifted the occluder there: a chosen avatar's legs behind a table showed
through its own long hair.

The layer no longer uses avatar ids:
- the warm-up keeps its opaque depth (no alpha-blend depth pre-pass anywhere
  in the sequence, so hair is not in it);
- each avatar-only recording takes the warm-up's surface at a pixel as one of
  the layer's avatars when its own avatar-only depth matches it within the
  tolerance somewhere in the 3x3 pixels around (the recordings are jittered by
  up to half a pixel). Anything else there is world: the occluder. It covers
  avatars on the cluster path, avatars the layer leaves out (drawn in the
  warm-up only) and hair over a table;
- depths are compared as linear view distances through the projection's z
  terms. The 0.5 % tolerance relative to 1/z became about a metre in an
  orthographic view, where walls and tables close in front of an avatar
  failed to cut it;
- the seen set goes: the per-pixel holdout already cuts an avatar a wall hides
  entirely. So do the host-written slot bits and the warm-up's id pass.
Translucent (alpha-blended) world surfaces, such as glass and blended
foliage, no longer cut the avatars; opaque and alpha-masked ones do.

Test-Plan: With the default cluster LOD settings (RenderVKClusterLOD and
  RenderVKClusterRigged on), take the transparent avatar layer of a mesh
  avatar, antialias on:
  - no see-through lines along an arm in front of the body, and the head
    and body behind long hair are whole;
  - another avatar on the cluster path in "All" or ticked in "Selected"
    shows in the layer;
  - stand behind a table with long hair hanging in front of it: the legs
    behind the table are cut, the hair over the table stays soft;
  - in the orthographic camera, a wall a few centimetres in front of an
    avatar cuts it.
Test-Settings: RenderVKClusterLOD = 1, RenderVKClusterRigged = 1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three loose ends of the layer's renderer half:
- vkSnapshotFit budgeted the renderer's targets plus the accumulators per
  pixel, not the layer's warm-up depth and occluder (4 B/px each). Those were
  also allocated before beginOffscreen's accumulators, so under memory pressure
  they could cost the shot its antialiasing. vkSnapshotFitAvatarLayer budgets
  them, and they are now allocated after the accumulators. snapMatteInit
  creates the pipelines before anything is allocated.
- Without its buffers the layer rendered on with no world cut. An avatar
  behind a wall would show, so the shot now fails (logged).
- If a recording could not run its holdout (a case that should not happen:
  the scene targets or the warm-up's depth not matching the shot), the
  straight-alpha tonemap still ran over alpha that was not coverage, and the
  log said the background stays opaque. That recording now tonemaps as usual
  and the shot fails, logged once.
The CPU no longer writes any buffer the GPU writes: the slot-bit buffer went
with the avatar ids.

Test-Plan: Snapshot floater, the transparent avatar layer at the largest size
  the card allows (the floater notes when it is reduced): the PNG comes out,
  antialiased, never with a world-coloured or opaque background. If the shot
  fails, the log names the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An RLVa vision sphere stays on every snapshot. Its passes (the sphere and the
final pass) work on the finished picture and write opaque pixels, so the
layer came out as an opaque PNG with flat mattes, and only the log said why.
Keeping the alpha through them would change those passes on every normal
frame. So the layer is now refused while a sphere is on, with a notification
shown at most every 20 seconds, because the floater re-renders its preview
often.

Also:
- the scope tooltip says your shadow and reflection stay in world snapshots
  when you untick yourself under "Selected";
- clRigLists' comment is back above it;
- a header comment line is re-wrapped.

Test-Plan: Under an RLVa @setsphere restriction (a vision-sphere test
  object), pick the transparent avatar layer in the Snapshot floater: a
  notification explains the layer is not available while the restriction is
  on, and no transparent PNG is saved. A colour snapshot still works and shows
  the sphere. Hover the "Avatars:" combo: the tooltip mentions the shadow and
  reflection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TheSlowly force-pushed snapshot-avatars-1328 from 920d11e3d6 to 9132304638 2026-10-31 23:22:55 +00:00 Compare
Author
Collaborator

Rebased onto current main (9132304638). The only conflict was the test registrations in indra/newview/CMakeLists.txt, and I kept both sides. The commits are otherwise unchanged.

Rebased onto current main (9132304638). The only conflict was the test registrations in `indra/newview/CMakeLists.txt`, and I kept both sides. The commits are otherwise unchanged.
Owner

Thanks! All of this is on main through the Beta 6 port, with main-side adaptations (plus 287e381f58 on top), so merging would only undo those. Closing as landed.

Thanks! All of this is on main through the Beta 6 port, with main-side adaptations (plus 287e381f58 on top), so merging would only undo those. Closing as landed.
sky_line closed this pull request 2026-11-03 00:27:30 +00:00

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sky_line/Slipstream!1341
No description provided.