Skip to content

CSG and Transparency

TeoLim edited this page Oct 14, 2026 · 34 revisions

CSG and Transparency

NWBLot integrates CSG, transparent rendering, and refractive caustics into the material-driven renderer. CSG receivers and cutters are ECS components; shape definitions and shader modules are cooked assets; transparent rendering uses AVBOIT-style passes that share the same scene lighting and material infrastructure as opaque rendering. Hardware ray tracing is optional; see Platform and GPU Support.

CSG Components

CSG receiver components:

  • StaticCsgMeshComponent
  • SkinnedCsgMeshComponent

Both derive receiver behavior from CsgReceiverComponent:

  • receiver group
  • enabled flag
  • affect opaque pass
  • affect transparent pass

CSG cutter component:

  • receiver group
  • shape type
  • world-to-shape transform
  • shape-to-world transform
  • parameter bytes
  • active flag

Receiver groups let cutters affect only selected receiver sets.

Built-In Shapes

Built-in shape assets:

  • plane
  • box
  • sphere
  • capsule

Each cooked shape entry provides a shape name, shader module include, eval include, and generated module include. At runtime the shape registry pairs each shape with a descriptor (CsgShapeTypeDesc: parameter byte size, default parameter bytes, and a required CPU bounds callback) and assigns it a shape type id on the registry wrapper (CsgShapeTypeInfo). The compact GPU id is a deterministic projection of the canonical shape-name hash, not a registration-order ordinal, so cook and runtime agree even when registration order differs. Both sides reject a collision; registration also rejects a null bounds callback.

Generated CSG include files allow renderer shaders to evaluate the active registered shape set consistently. The cooker preflights canonical generated include paths before clearing or writing the tree, so two different modules cannot silently overwrite each other's generated include.

CSG Frame State

Renderer prepare builds CSG frame state when CSG candidates exist in the world. CsgFrameState itself carries work flags and counts:

  • opaque static work
  • opaque skinned work
  • transparent static work
  • transparent skinned work
  • receiver and cutter counts

Receiver/cutter grouping is held separately in a CsgFrameReceiverLookup (a map from receiver group to cutter ranges), and the affected screen-space work region lives on the GPU-side CsgFrameGpuData, not on CsgFrameState.

Preparation resolves each candidate cutter against the receiver's local/world bounds and its registered shape bounds. A finite cutter that does not intersect a finite receiver is skipped; an intersecting finite cutter clips the CSG work bounds, while an unbounded cutter uses the receiver bounds. A valid but non-finite receiver bound cannot safely cull, so it remains in the CSG path rather than being rejected by an invalid early-out. This keeps peel, clear, and cap work limited to meaningful receiver/cutter overlap without losing viable work.

If no CSG candidates exist, renderer work uses an empty CSG frame state and skips interval work.

CSG Heap Contract

CSG passes use the global Bindless Descriptor Heap, not pass-local resource descriptors. One uniform CsgClipContextSlots heap entry selects the persistent receiver-range and cutter buffers, material/mesh inputs, deferred target-generation slots, and interval-sample state.

CSG and AVBOIT preparation capture the uploaded frame state, receiver surfaces, materialized draw items, and exact graph resource identities. Typed graph tasks own interval clears, peel, receiver-span generation, interval combination, and cap states. Receiver-span and combine tasks retain the shared aggregate timing endpoint. Missing required identities reject preparation; recording never substitutes a native combined recorder or publishes partially prepared state.

Receiver-span construction handles two valid front/back events for the same receiver through a scalar common path. It selects entry and exit by face rather than raster arrival order and avoids the dynamic event array and sort. The strict exit-greater-than-entry comparison, inherited flags, and span capacity/error behavior match the generic path; no new finite-value restriction or depth tolerance is introduced. Foreign receiver IDs or invalid face values continue through the generic count-bounded active-prefix path.

The generic path loads lossless uint3 event records containing the full receiver ID, face value, and FP32 depth bits. It groups by receiver ID and sorts by depth within each group, then walks winding independently for every receiver. Overlapping receivers therefore retain their own spans instead of losing events to another receiver's open winding. Nested shells merge only within the same receiver; unmatched boundaries still report ambiguity, and strict depth comparisons and capacity/overflow reporting remain in effect. The logical record size does not establish the compiler's actual local-memory allocation or a timing gain. Interval combination consumes each receiver's spans by ID and separately sorts removed-interval candidates for rendering.

AVBOIT occupancy, extinction, and accumulation consume required prepared draw partitions, CSG frame data, CSG graph resources, and immutable mesh-frame bindings. The graph declares receiver-event/sample-image and CSG clip-buffer states. A clipped draw phase without its required interval producer fails immediately after gathering, before geometry declarations, material uploads, or captured-snapshot publication. Empty phases retain their no-work behavior, and unclipped phases do not require an interval producer. Frame, geometry, generated-output aliasing, and shared-output interleaving remain current contracts.

Resource validation creates or recreates CSG targets and their heap handles before rendering begins. If a persistent CSG buffer grows, the renderer publishes a fresh heap handle and retires the old handle through the in-flight quarantine; draw and dispatch paths only consume validated slot contexts.

Opaque CSG Flow

Opaque CSG work during renderer command recording:

  1. gather regular, CSG, and receiver-surface draw partitions
  2. begin the first G-buffer raster segment with attachment clears and clear CSG interval targets for the affected region
  3. upload CSG frame buffers
  4. upload interval sample state
  5. dispatch interval peel
  6. render receiver surface
  7. build receiver spans
  8. combine intervals
  9. render CSG draw items
  10. render interval caps

This lets the material pass and CSG system cooperate without moving CSG into a separate renderer unrelated to material state.

G-buffer color and depth initialization uses attachment load operations in the first raster segment. Continuations load the preserved attachments, which avoids standalone full-surface transfer clears and keeps the path friendly to tile-based GPUs without requiring vendor-specific tile extensions.

The opaque CSG path branches out of normal material rendering only when receivers and cutters participate in the current frame:

flowchart TD
    Components["Receiver and cutter components"]
    Prepare["Build CSG frame state"]
    Gather["Gather draw partitions"]
    Clear["Clear affected deferred and interval targets"]
    Upload["Upload CSG frame buffers"]
    Peel["Dispatch interval peel"]
    Receiver["Render receiver surfaces"]
    Spans["Build receiver spans"]
    Combine["Combine intervals"]
    Draw["Render CSG draw items"]
    Caps["Render interval caps"]
    Continue["Continue shadows, lighting, and composite"]

    Components --> Prepare --> Gather --> Clear --> Upload --> Peel --> Receiver --> Spans --> Combine --> Draw --> Caps --> Continue
Loading

CSG Receiver Surface

Receiver surface rendering is separate from regular opaque material draws. It exists so interval computation can know where receiver surfaces are in screen space and where cutter intervals should affect the final image.

CSG Caps

Cap rendering fills surfaces exposed by CSG operations. Cap behavior must stay attribute-aware and consistent with receiver material/shading expectations.

Perspective cap positions use the shared camera-plane ray in impl/assets/graphics/mesh/view_ray.slangi and the existing FP32 camera-radial exit distance in removed-interval depth .w. The helper loads the stored view matrix as a whole value and extracts its basis with mul on coordinate-axis vectors, preserving the compiled UniformBuffer layout and raster multiplication convention. Projected cap depth in .y remains the raster-ordering value. For a neighbor repair, the source distance is transferred onto the output ray while preserving camera-space depth. Direct shadows select this physical position only when the cap's projected depth equals the current D32 depth; cap fill draws last with LessOrEqual, preserving foreground ownership. Ordinary receivers retain their existing half-precision G-buffer quantization bounds.

The shadow graph enables cap sampling only for current opaque CSG work and retains the exact current view and interval sample-state buffers. The uploaded work rectangle bounds all interval reads, and the graph declares constant-buffer reads plus storage-image reads of removed depth, data, and count. These uses order shadow consumers before later transparent interval reuse of the same resources. Finite positive radial distances and the existing bounded receiver-plane check remain required; these contracts do not replace visual and timing qualification.

A ready light-space snapshot with CSG enabled also selects the CSG shadow packet-tail policy. Ordinary prepared-shadow descriptors retain their existing routing, and extra hardware material/geometry reads require both hardware shadow support and admitted CSG. Tail stages explicitly depend on their immediate predecessor and use compatible queue routing; the CSG tail permits merging across the existing consumer frontier. A bounded diagnostic-word copy follows the fold in that same accepted packet, with the fold retained as the shadow publication endpoint. The copy retains the exact context/readback/frame identity, restores the context to UAV, and publishes CPU-readback completion only from its accepted submission token. Resource return-state snapshots follow actual packet-final states. Strict topology validation rejects a separated diagnostic tail and diagnostics on the monolithic path; it is not disabled to make a graph admissible.

The dedicated surfel frame-constant buffer uses four float4 lanes (64 bytes). cellSizeRadiusBiasAccum holds the existing FP32 cell size, radius, normal bias and accumulation cap; its sole CPU producer retains all current frame values and conversions. Live and snapshot surfel records use a 96-byte stride with exact signed cell ownership. See GPU payload packing for the shared CPU/shader layout contract.

CSG Ray Geometry and Direct Shadows

GI and direct CSG shadow fallback share the analytic ray geometry and selected hardware/software collectors under impl/assets/graphics/csg. Cooked membership identifies closed components in final ray-triangle order. Closed components use oriented winding and subtract the cutter union to retain solid spans and generated walls; open or zero-volume components remain clipped surfaces without invented solid spans or caps. Mixed receivers integrate retained open surfaces independently of their closed solid and cap boundaries (nwbLightSpaceIntegrateInstance in impl/assets/graphics/shadow/light_space_resolve.slangi). Boundary reconstruction uses the receiver material and preserves transparent interface and absorption semantics.

GI traces current receiver poses and cutter data in its own context. Light-space direct fallback uses the retained capture geometry and cutter transforms. Across rigid capture reuse, it maps the retained ray into current selected geometry without normalizing away its ray parameter. Geometry or material changes invalidate reuse. Receiver bounds are broad-phase rejection data, not replacement occluders: failed map coverage must not fill a carved cavity with an enclosing-box blocker. Invalid or over-budget geometry reports a diagnostic and retains conservative failure handling rather than silently treating the uncut receiver as exact CSG.

The smoke sequence driver tests/smoke/csg_gi_temporal_smoke.py captures 32 frames from one process. Live rendering first reaches graphics frame 360, then continues for at least 30 steady-clock seconds before logging the actual first capture frame. Startup preparation, compilation, and asset loading do not count toward that interval. Captures follow firstCaptureFrame + sampleIndex * frameInterval; --frame-interval accepts 1 through 64, defaulting to 8 (a span of 248 frames). Interval 1 records consecutive frames with serialized readback completion. The harness requires the warm-up evidence and exact relative source-frame sequence. This is a settled GI acquisition protocol, not a renderer convergence guarantee.

Full, direct-only, and indirect-only views support static or deterministic animated poses, natural or disabled hardware tracing, optional lagged lighting, and a GPU timing sidecar. Dense per-frame regional metrics record horizontal/vertical RGB channel jumps as well as temporal measurements, retaining the worst frame rather than hiding stripes in an average. Static measurements keep the 80 x 16 box_upper_band centered at (836, 290); animated measurements replace only that region with the 48 x 16 box_inner_wall centered at (832, 316), covering [808, 856) x [308, 324). Logged acquisition controls must match analysis requests. See tests/smoke/software_raytracing.md for commands and artifact contracts. Without a check option, success establishes sequence/log/readback admission. Optional --check-quality applies the fixture's finite lighting, temporal, spatial-edge, tint, and background limits. Static --view normal or position with --check-geometry checks four analytical cap pixels in each frame (128 probes per sequence), with display-quantization tolerances. Static --view cap_state with --check-cap-state requires produced, visible caps that own completed depth at 16 required pixels per frame (512 probes per sequence). CTest registers fourteen sequences: static full at interval 8; consecutive full/direct/indirect/software at interval 1; animated natural/software at interval 8; lagged static full; static normal/position geometry probes; static natural/software cap-state probes; and static natural/software overlapping span probes. They request GPU validation and serialize display use. These scene-specific gates do not establish general image quality, complete optical correctness, or a performance improvement; interval-1 timing remains specific to the serialized capture workload.

The smoke fixture also selects test-owned cap_state, interval_state, event_state, event_data, event_order, and span_state materials. Cap-state RGB shows nonzero raw removed count, nearest radius-one cap visibility, and exact completed-depth ownership. Interval-state RGB shows nonzero receiver-span count, a nonzero ID in the first four peel layers, and positive back-cap alpha for one of those IDs. Event-state RGB encodes raw/front/back counts divided by 33, scanning at most 32 stored events and retaining raw 33 as the overflow sentinel. Span-state RGB encodes receiver span count, existing span flags, and raw receiver event count divided by 33; the harness requires its encoding marker and decodes each channel with round(channel * 33).

The fixture owns these diagnostic materials and nine BXDF families under tests/smoke/csg_visible_assets, separate from the shared tests/smoke/assets catalog used by ordinary smoke/performance scenes. Its executable asset dependency runs tests/smoke/prepare_fixture_asset_root.py to verify and copy the disjoint union into Testing/csg_visible_generated/<config>. The cooker receives engine impl/assets plus this single project root, preserving all project/... references; resources remain in Testing/csg_visible_smoke_runtime/<config>/res. Source resolution chooses the first matching virtual root without checking a later project root, so two project roots cannot replace this preparation. The helper rejects identity collisions, unsafe root overlaps, and unexpected existing files, verifies the complete file set and payloads, and never deletes files. Isolating the catalog keeps fixture-only dispatch families out of ordinary scenes.

Event-data/order red is 1 when a receiver span exists; a missing span exposes its existing flags divided by 33 (ambiguity 1, span overflow 2, event overflow 4). Event-data green tests exactly two events with matching front/back receiver IDs for the first nonzero peel ID; blue tests finite matching depths with back greater than front. Event-order shows raw event 0/1 faces as front 1 or back 0.5 only after the same-frame pair passes receiver, opposite-face, finite-depth, and ordered-depth validation. Invalid pairs show zero face channels and remain null in decoded face fields. The harness requires the appropriate encoding marker and reverses presentation before decoding observations at the four geometry probes and a 5 x 4 capsule patch into every frame's manifest. These views inspect existing current-frame resources through ordinary test materials; they add no production diagnostic selectors and accept neither lighting-quality nor static-geometry gates. The cap-state gate requires all three decoded channels at the four centers and the 12-pixel capsule rim block [784, 787) x [604, 608) in every capture; eight outer-rim pixels remain observational. Its manifest retains cap_state_required_probes, cap_state_gate_contract, and cap_state_failures. Displayed state alone and these finite probes do not establish general topology, producer visibility, or complete cap coverage.

The driver's optional --overlapping mode adds one static plane cube receiver at the same x/y and z + 0.125, sharing its receiver group, cutter, and diagnostic material. The log records that exact duplicate and analysis matches the logged mode to the requested controls. --check-spans requires static span_state with this mode: the plane center must decode two spans, zero flags, and four events, while the other three centers must each decode one span, zero flags, and two events in every capture. The checked manifest retains span_required_probes, span_expectations, and span_failures, alongside encoding and overlap contracts. CTest registers these multi-event checks for both natural and software routes.

Synchronous CSG ray and receiver-event helpers borrow read-only arrays with Slang __constref and mutate caller-owned arrays with __ref. Count-bounded loops visit only initialized active entries; inactive capacity does not require a whole-array copy or evaluation. Reference directions, ordering, capacities, failure behavior, and caller lifetime remain part of the contract. Configured compiler output and runtime checks are required to attribute a performance result to this change.

CSG qualification includes a matched scene with no CSG objects, comparing CPU/frame time and affected GPU passes against unaffected controls after settling. Cap-specific CPU snapshots and graph bindings must stay conditional on current opaque CSG work, and ordinary variants must compile out unused CSG arrays and constants. Preserve resolution, route, view, binaries/assets, timing publication, and capture mode across comparisons; shader equivalence alone does not establish unchanged ordinary-scene FPS.

Transparent Rendering

Transparent materials use the AVBOIT path. Material assets still drive transparent rendering; transparent rendering is not a separate untyped shader path.

For surface-authored transparent materials, the cook generates transparent occupancy, extinction, and accumulate pixel shaders from the same .bind and .surface pair. The accumulate shader rejects unused fragments before material evaluation, then evaluates the material .surface hook, builds a BXDF surface, and dispatches the material's .bxdf before applying AVBOIT accumulation. Transparent or refractive materials require a project surface hook. Explicit stage maps are accepted only for opaque, non-refractive materials. A cooked transparent material must carry all three AVBOIT shader references, while an opaque material carries none; build, serialization, and load validation reject a partial set.

AVBOIT resources include:

  • low-resolution raster/coverage targets
  • depth warp targets
  • extinction accumulation
  • integration/transmittance targets
  • transparent accumulation
  • target heap handles and slot records
  • final integration with deferred targets

Transparent Frame Flow

Transparent renderer flow:

  1. prepare AVBOIT pass resources if transparent renderers exist
  2. clear AVBOIT targets when transparent data exists or stale targets need clearing
  3. build transparent CSG intervals when CSG participates in transparent work
  4. render the occupancy material pass
  5. dispatch depth warp
  6. render the extinction material pass
  7. dispatch integration
  8. render the accumulate material pass
  9. combine the transparent result with the deferred/composite output

The dispatches interleave with the material passes (occupancy -> depth warp -> extinction -> integration -> accumulate) rather than all following them.

Transparent rendering shares scene camera, light, material, and CSG state with the opaque path.

Transparent shadowing is also material-driven. Transparent occluders contribute RGB transmittance from the same surface hook used by the raster paths. Staged hardware evaluation and software GPU-BVH traversal consume the cook-generated surface dispatch for optical parameters. On RT hardware, transparent occluders use staged hardware ray queries (gather/evaluate/overflow-continuation passes collecting per-pixel crossings and evaluating RGB transmittance); when the staged path cannot run, the software traversal applies because the direct RayQuery path is opaque-only; when soft transparent resources are ready, the colored transparent signal is denoised with the RGB soft-shadow resolve into HalfA while the opaque wavelet output stays in HalfB, and one combined upsample resolves both with a shared float4 guided fit. The combined pipelines are prepared on software frames too (prepareSoftCombinedResolvePipelines in both preflight branches), so this fusion is the same on both backends. Transparent-stage failure still resolves the retained opaque buffer through the original scalar upsample. If that soft fold is unavailable, a full-resolution software traversal multiplies transparent color onto the hardware-written opaque shadow mask.

On the software path, transparent soft shadows can use a separate half-resolution colored trace, the RGB soft-shadow resolve, and a final multiply fold onto the soft opaque visibility. If the soft transparent path is not available, the renderer falls back to the adaptive transparent fold (coarse transmittance feeding the adaptive resolve modes, or the full-resolution adaptive fallback).

Refractive Caustics

Refractive caustics are tied to material authoring, not to a separate hard-coded glass path. A material marked refractive is eligible as a caustic caster; its .surface hook supplies the shader-side refraction and transmission values. Lights with caustic slots provide the emission source.

The renderer supports both capability-selected backend paths:

  • hardware ray tracing traces caustic photons through the TLAS
  • software fallback traces caustic photons through the software scene BVH and per-mesh BVHs

Both paths target refractive instance bounds, splat photon flux into the same RGB integer accumulator, and use the same resolve pass. The resolve writes screen-space caustic irradiance that deferred lighting adds before tone mapping. The irradiance target is cleared to black each frame, so absent resolved caustic work contributes nothing. The accumulator uses the renderer's internal 0.85 temporal decay, persists as splat-space history, decays before each new photon splat, and is normalized in the resolve so static brightness stays stable.

The caustic resolve chain skips inactive regions throughout. Both caustic producers use the same two-frame checkerboard emission contract. Each frame traces half of the full photon grid selected by frameIndex & 1, and the temporal accumulator combines the complementary phases while preserving energy through the per-frame photon-count normalization. CausticQualitySettings::photonGridDivisor (1, 2, or 4) divides each photon-grid dimension before the phase split (area factors 1, 1/4, 1/16), keeping temporal phases and filtering unchanged. Emission-target preparation drops opaque (non-refractive) instances before mesh resource resolution, replacing the former degenerate-target skip counter.

Caustic resolve owns separate prepare, wavelet, and upsample pipelines: preparation keeps the dynamic program while wavelet and upsample use constant stage variants. Upsample threads do not reserve the wavelet shared-memory tile. Dilations 8 and 16 use a direct-texture wavelet variant without the small-dilation shared tile. The chain uses five dilations and seven dispatches with the shared 56-byte push layout and filtering equations.

Skinned refractive meshes use a per-frame normal repack before trace work so shadows and caustics bend from the live deformed normals.

Generated geometry is reused across compatible transparent passes (AvboitGeneratedGeometryReuse): occupancy, extinction, accumulation, and refraction capture share one compute-emulation expansion when their programs agree, instead of expanding per pass.

The transparent path uses the same renderer state, then runs the AVBOIT sequence before final composition:

flowchart TD
    Transparent["Transparent materials or transparent CSG"]
    Prepare["Prepare AVBOIT resources"]
    Clear["Clear stale AVBOIT targets"]
    Csg["Build transparent CSG intervals when needed"]
    Occupancy["Occupancy material pass"]
    Warp["Depth warp dispatch"]
    Extinction["Extinction material pass"]
    Integration["Integration dispatch"]
    Accumulate["Accumulate material pass"]
    Composite["Composite with deferred output"]

    Transparent --> Prepare --> Clear --> Csg --> Occupancy --> Warp --> Extinction --> Integration --> Accumulate --> Composite
Loading

Camera Refraction

Transparent materials with a finite surface IOR above 1.0 participate in camera-visible refraction (nwbRefractionIorEligible in impl/assets/graphics/refraction/ior.slangi). RendererSystem::setRefractionEnabled(false) restores ordinary AVBOIT, and setRefractionHardwareTracingEnabled(false) selects the screen-space route without changing device capability reporting. Both settings default to enabled.

The renderer retains one primary refractive instance per pixel. A depth-tested raster pass captures its normal, IOR, absorption tint, and transparent-pass instance identity after opaque visibility and transparent CSG intervals are ready. The primary instance is omitted from AVBOIT occupancy, extinction, and color accumulation; the resolve owns its optical transmission. Other transparent fragments accumulate into foreground or background attachments according to the captured primary depth, so foreground particles and panels keep their camera pixels. Clear glass may have zero renderCoverage: coverage is the AVBOIT opacity parameter, not an optical transmission mask.

The resolve has two capability-selected forms:

  • hardware: the shared scene TLAS associates the raster entry with an instance, finds the volume exit, and traces the transmitted background. Interface admission runs before material reconstruction (impl/assets/graphics/refraction/interface_hit.slangi): the entry admits the same nearest front-face geometric hit and the exit admits the same-instance back face, so rejected faces/instances never pay for material reconstruction. The usual path then issues three nearest-hit queries; the bounded loop permits at most seven, including internal reflections and additional ordinary transparent background layers. Absorption uses the traveled distance inside the primary volume with Fresnel transmission at entry and exit, and opaque screen color is reused only after validating visibility against the G-buffer.
  • screen: no ray-query work. At most 24 depth-march steps with a straight-through fallback when no usable screen hit is established; without a geometric exit, absorption uses a documented unit-thickness sheet approximation. CSG primaries also use this route because the raw triangle TLAS does not encode boolean-cut exits.

Capture and resolve run at full resolution for sharp glass. The default-format targets add roughly 42 bytes per pixel alongside existing AVBOIT resources. Every resolve thread checks the capture mask first, so pixels without a refractor issue no rays. Resizing replaces the complete target generation; disabling refraction clears captured identity/depth and resolved validity. The current version has no dispersion, rough-transmission sampling, denoising, reflected radiance lobe, or arbitrary nested media stacks; these are deliberate quality limits. See docs/refraction.md for the full contract and Reflections for the reflection radiance the resolve consumes.

Deformable CSG Editing

Deformable editing keeps preview and commit viability aligned across sequential cuts. RebuildSequentialCuts, PreviewCsgDeformCuts and CommitCsgDeformCuts return Expected<CsgDeformStats, CsgDeformFailure>; failure carries the viability reason and original diagnostic statistics. Geometry vectors retain their owning arenas and capacity reuse. Wrong commit-output ownership returns InvalidOutputArena before changing either destination. Both paths run one deterministic CPU-side rebuild (impl/ecs_csg/deform_*), so a previewed shape either commits identically or fails with the same viability reason. There is no separate preview mesher: CheckCsgDeformCutsViability runs the full pipeline rebuild and returns its viability, PreviewCsgDeformCuts rebuilds into scratch storage, and CommitCsgDeformCuts validates that its output vectors are owned by the commit arena, calls the preview entry point into scratch with the same pipeline, and then copies the result into global storage.

Domain split on main:

  • deform_types.h — shared POD and thresholds. CsgDeformVertex (position, normal, tangent, uv0, color), CsgDeformTriangle, CsgDeformShape (shape type, world-to-shape transform, packed parameter0), CsgDeformCutDesc (shape plus active flag), CsgDeformBuildOptions (distanceEpsilon, fillCaps), CsgDeformViability plus CsgDeformViabilityReason, and CsgDeformStats. One threshold table serves every stage: default distance epsilon 0.00001 floored at runtime to s_MinEpsilon (0.0000001, see CsgDeformValidator::ShapeEpsilon), split-denominator/normalize/loop-area epsilons, 1 << 20 vertex/triangle capacity caps (s_DeformCapacityShift), edge-key/hash constants, loop constants, and reserve constants (s_RebuildReservePerCut 16 per cut, s_KeptReserveMultiplier 2, s_ReserveSlack 1), plus the canonical edge key that keeps (a, b) and (b, a) identical for walls and caps.
  • deform_validator — input gate shared by preview and commit: FiniteFloat/FiniteVertex checks over all vertex attributes, ValidTopology over (triangles, triangleCount, vertexCount) index bounds, ValidOptions (epsilon must be strictly inside (0, 1)), ShapeEpsilon (floor to s_MinEpsilon), and SaturateVec (SIMD vector clamp for blend weights). The pipeline applies the gate in order — empty input, oversized input (TooLarge), bad options, non-finite input, bad topology — before any cutter, wall, or cap work runs.
  • deform_cutter_field — cutter classification and per-vertex signed distances for plane, box, sphere, and capsule, mirroring the GPU SDF evals, with the shared epsilon snap (|distance| <= epsilon reads as zero). Classification compares against the canonical built-in cutter Names in impl/assets_csg/shape_id.h (engine/csg/plane, engine/csg/box, engine/csg/sphere, engine/csg/capsule), shared with the runtime shape registry so CPU and registry naming cannot drift. ShapeDistances dispatches once per cut to a distance-function pointer, then runs one shared per-vertex loop (world-to-shape transform with affine w = 1 plus SDF eval on SIMD lanes, only the snapped scalar distance crosses back), so preview and commit observe identical distances. Unknown shape types report InvalidCutter; a non-finite evaluated distance reports NonFiniteInput.
  • deform_wall_builder — seam-safe, attribute-aware wall handling. Splits triangles at the zero crossing through the shared edge-split map (source vertices are never welded; one cached split vertex per canonical edge), interpolates position, normal, tangent, UV, and color for new edge vertices with saturated blend weights, renormalizes with a deterministic degenerate fallback that preserves w/tangent handedness, and emits the kept side (distance >= 0 after the snap). Split-edge denominator failures and vertex-capacity overflow fail the cut.
  • deform_cap_builder — cut-boundary cap fill. CollectBoundaryEdges counts canonical edge uses and keeps use-count-1 edges in emitted triangle order; OrderBoundaryLoop walks them undirected from the smallest vertex index and enforces single-closed-ring closure plus loop-vertex uniqueness (scratch-backed HashSet<u32>); CapNormal accumulates the fan area and rejects degenerate area; FillCapLoop averages loop position/UV/color for the center vertex, applies the loop normal, enforces the single cap-orientation rule (reverse kept winding: emit (center, second, first) so caps face the cut), and emits a deterministic fan. FillCutCaps peels one closed loop at a time using a scratch-backed HashSet<u32> membership set for O(1) adjacency: open boundary chains (e.g. cuts across an open sheet) stay viable cap-free, while a closed-but-degenerate loop reports CapLoopFailed.
  • deform_pipeline — sequential-cut orchestration (CsgDeformPipeline::RebuildSequentialCuts). Validates the input gate above, reserves output/scratch storage from the input sizes plus per-cut reserve, copies the input into the output buffers, then applies each active cut in order as one wall rebuild plus one cap fill (when fillCaps is set). After every cut it enforces non-empty kept geometry and capacity caps (NoKeptGeometry/TooLarge); at the end it re-checks output non-emptiness and output finiteness, then returns admitted statistics. Failures retain input counts and leave output/applied-cut/cap counts unpublished.
  • deform_edit — stable facade preserving the existing include path: CheckCsgDeformCutsViability, PreviewCsgDeformCuts (scratch out-buffers), and CommitCsgDeformCuts (arena-ownership check, same scratch rebuild via the preview entry point, then copy to global out-buffers).

SIMD boundary rule (behavior-preserving, viability-identical): Load/Store on deform storage runs only at the storage entry points (MixVertices, NormalizeDeformVertex, CapNormal, FillCapLoop, ShapeDistances); per-lane math lives in SIMD-domain cores that take and return SIMDVectorSingle/SIMDMatrix (MixAttributeVec, NormalizeDirectionVec, KeepWVec, TangentHandednessVec, AccumulateFanAreaVec, ScaleCenterVec, CapCenterNormalVec, PlaneSignedDistanceVec, BoxSignedDistanceVec, SphereSignedDistanceVec, and CapsuleSignedDistanceVec). Storage stays in Float# memory types.

Cutter shapes pack the same fields as the shader cutters (parameter0: plane xyz normal plus w distance; box xyz half extents; sphere x radius; capsule x radius plus y half height).

Viability reasons: Ok, EmptyInput, NonFiniteInput, InvalidTopology, InvalidCutter, CapLoopFailed, NoKeptGeometry, TooLarge, InvalidOutputArena. Stats report input/output vertex and triangle counts plus applied-cut and cap-triangle counts. Coverage on main: tests/unit/csg/csg_tests.cpp (SequentialCutsPreserveEarlierHalfSpaceAndUnweldedSourceVertices, CsgDeformCutViabilityRejectsDegenerateCommit) via nwb_ecs_csg_tests.

flowchart TD
    Facade["Check / Preview / Commit facade"]
    Validate["Validator: finite input, topology, options"]
    Loop["For each active cut in order"]
    Wall["Wall builder: epsilon snap, edge-cache split, kept side"]
    Cap["Cap builder: boundary loops, orientation check, fan fill"]
    Publish["Publish viability and stats"]

    Facade --> Validate --> Loop --> Wall --> Cap --> Loop
    Cap --> Publish
Loading

Clone this wiki locally