Assembly structure, data flow, GPU struct alignment, shader internals, and component resolution patterns.
RCE ships as three assemblies. The core assembly contains all engine logic and has no render-pipeline dependency. Two optional assemblies provide pipeline-specific integration — only one compiles per project depending on which Unity render pipeline package is installed.
| Assembly | Purpose | Compile Condition |
|---|---|---|
| RCE.Runtime | Core engine: data types, loadout management, SDF rendering, animation, transitions, tracer system, UI, serialization, and all shared utilities. References only Unity.RenderPipelines.Core.Runtime. |
Always |
| RCE.Runtime.URP | Contains RCERendererFeature — a ScriptableRendererFeature that injects RCE render passes into URP's render graph. |
When com.unity.render-pipelines.universal is present |
| RCE.Runtime.HDRP | Contains RCEHDRPCustomPass — a CustomPass that injects RCE rendering into HDRP's After Transparent pass. |
When com.unity.render-pipelines.high-definition is present |
Unity's assembly definition system compiles each .asmdef independently. By isolating pipeline-specific code into optional assemblies with version defines, the core RCE.Runtime assembly never references URP or HDRP types directly. This means the same package installs cleanly in both URP and HDRP projects with zero user configuration — no scripting defines, no conditional compilation flags, no "choose your pipeline" setup wizards.
The dependency graph is strictly one-directional:
Pipeline assemblies reference the core; the core never references pipeline assemblies. Pipeline assemblies never reference each other. This guarantees that removing a pipeline package never breaks compilation.
The engine is organized by responsibility under Assets/RCE/Runtime/. Each folder maps to a distinct concern:
Assets/RCE/Runtime/ ├── Bootstrap/ # RCEInstaller, service locator, startup ├── Core/ # Interfaces (ILoadoutManager, IDataPathProvider) and constants ├── Data/ # Enums & value types (ShapeType, ShapeData, GapMode, AnimationMode, etc.) ├── Loadout/ # LoadoutData, LayerData, CategoryData, TransitionSettings, serialization ├── Rendering/ # ReticleRenderer, LoadoutRendererBridge, GlobalFXBridge, GridRenderer │ └── # CategoryTransitionAnimator, ShapePropertyAnimator, CustomSDFGenerator ├── Tracers/ # TracerManager, TracerData, TracerSDF shader integration ├── Palettes/ # PaletteData, preset color palettes, palette serialization ├── Utilities/ # Easing, WaveformEvaluator, SDFEvaluator, UndoManager, MathHelpers ├── UI/ # RCEEditorController, UXML layouts, USS stylesheets, theme files ├── Shaders/ # ReticleSDF.shader, ReticleSDFCore.hlsl, GridSDF, LayerConnectorLines, TracerSDF ├── Resources/ # ExactPolylineSDF.compute, materials, runtime-loaded assets └── URP/ # RCERendererFeature.cs (separate assembly: RCE.Runtime.URP)
| Folder | Contents |
|---|---|
| Bootstrap | Engine initialization. RCEInstaller registers services with the service locator. Entry point for both editor and runtime contexts. |
| Core | Public interfaces that decouple engine subsystems. ILoadoutManager is the primary API surface for game developers. IDataPathProvider abstracts file storage paths. |
| Data | All enums (ShapeType, GapMode, AnimationMode, TransitionType, etc.) and the critical ShapeData struct that must match the HLSL layout byte-for-byte. |
| Loadout | Managed-side data model. LoadoutData is the root; it contains CategoryData → LayerData → ShapeAnimationEntry. JSON serialization with version migration lives here. |
| Rendering | The GPU pipeline. ReticleRenderer owns the StructuredBuffer and issues draw calls. Bridge classes (LoadoutRendererBridge, GlobalFXBridge) sit between the data model and the renderer. Transition and animation processors live here. |
| Tracers | Bullet tracer/bolt rendering. Separate StructuredBuffer and TracerSDF.shader for projectile trails. |
| Palettes | Preset and user-defined color palettes. Serialized alongside loadouts. |
| Utilities | Shared math: easing curves (20+ types), waveform evaluation, C# SDF evaluation for edit-time anchor placement, undo/redo engine. |
| UI | The visual editor built with Unity's UI Toolkit. UXML layouts, USS styles, theme files (Dark/Light/Cobalt/Forest/HotPink), and the 7600-line RCEEditorController. |
| Shaders | All HLSL. The main ReticleSDF.shader contains two SubShaders (URP + HDRP) that share ReticleSDFCore.hlsl. Supporting shaders for grid, layer connectors, tracers, and custom shape overlay. |
| Resources | Assets loaded via Resources.Load at runtime. The ExactPolylineSDF.compute shader for custom shape SDF generation, default materials, and fallback assets. |
| URP | Single file: RCERendererFeature.cs. Lives in its own assembly definition (RCE.Runtime.URP.asmdef) that references URP and RCE.Runtime. |
Every frame, shape data flows through a five-stage pipeline from the loadout manager to the GPU. Each stage has a single responsibility and a well-defined handoff to the next.
| Stage | Class | Responsibility |
|---|---|---|
| 1. State & Emission | LoadoutManager |
Holds the active LoadoutData and responds to SetActiveState() calls. Emits the current category's layers as a ShapeData[] array, merging HUD shapes (always-on) with the active combat state's shapes. |
| 2. Transition | CategoryTransitionAnimator |
Intercepts state changes. If a transition is configured for this state pair, it captures the outgoing shape array as "from" and the incoming array as "to", then interpolates between them over the configured duration using the selected transition type (Morph, CrossFade, Pop, Slide, etc.) and easing curve. Outputs the interpolated ShapeData[] each frame until the transition completes. |
| 3. Animation | ShapePropertyAnimator |
Evaluates all active ShapeAnimationEntry definitions for the current shapes. Computes additive deltas (multiplicative for scale) from the configured waveform mode and applies them to the base ShapeData values. Handles delay, ramp in/out, loop modes, and animation sequences. Pauses during active transitions. |
| 4. Sanitize & Upload | LoadoutRendererBridge |
Receives the final animated ShapeData[], performs safety checks (NaN clamping, invisible shape zeroing), and calls StructuredBuffer.SetData() to upload the array to the GPU. |
| 5. Render | ReticleRenderer |
Issues a single DrawProcedural call that renders a full-screen quad. The ReticleSDF fragment shader reads the StructuredBuffer<ShapeData> and evaluates every shape's SDF per-fragment. |
Global layers (hit markers, damage flashes, persistent HUD widgets) flow through a parallel pipeline that shares the animation and rendering stages but uses its own bridge and renderer instance:
The GlobalFXBridge filters Global category layers by visibility state (shown/hidden/playing), handles fire-and-forget lifecycle, and emits only visible shapes. Because this is a completely separate renderer instance, global effects are never interrupted by combat state transitions on the main pipeline.
The ShapeData struct is the fundamental contract between C# and HLSL. It is 144 bytes, tightly packed with [StructLayout(LayoutKind.Sequential)], and must match the HLSL struct definition byte-for-byte. Any misalignment causes shapes to render with corrupted properties.
[StructLayout(LayoutKind.Sequential)] public struct ShapeData { public Vector4 color; // 16 bytes — RGBA fill color public Vector4 outlineColor; // 16 bytes — RGBA outline color (alpha > 0 enables outline) public Vector2 position; // 8 bytes — UV-space position public Vector2 scale; // 8 bytes — X/Y scale public float rotation; // 4 bytes — radians public float glowIntensity; // 4 bytes — glow brightness multiplier public float glowSize; // 4 bytes — glow distance falloff public float feather; // 4 bytes — edge softness (0 = hard) public int shapeType; // 4 bytes — ShapeType enum public int gapMode; // 4 bytes — bit-packed (see below) public int gapShapeType; // 4 bytes — bit-packed (see below) public int groupShapeIndex; // 4 bytes — boolean group operand index public Vector4 shapeParams; // 16 bytes — type-specific parameters public float gapCount; // 4 bytes public float gapThickness; // 4 bytes public float gapRotation; // 4 bytes public float taperAmount; // 4 bytes public int isOutline; // 4 bytes — true outline toggle public float outlineThickness;// 4 bytes public int customSDFIndex; // 4 bytes — Texture2DArray slice index public int _padding; // 4 bytes — alignment padding } // Total: 144 bytes
Two integer fields pack multiple flags into their bits to minimize struct size while maintaining 4-byte alignment:
| Field | Bits | Meaning |
|---|---|---|
gapMode | 0–5 | Gap mode enum: 0 = None, 1 = Radial, 2 = Grid, 3 = Lines, 4 = CenterCutout |
| 6 | AsSDF flag — Apply gap as SDF boolean (geometry subtraction) instead of alpha mask | |
| 7 | Invert flag — Reverse gap behavior: reveal only inside gap areas | |
gapShapeType | 0–7 | CenterCutout shape: 0 = Circle, 3–8 = polygon with N sides |
| 8 | LineBurst mode — 0 = ThroughCenter, 1 = Radial | |
Stadia uses the entire field differently: (symmetry << 4) | (capStyle << 2) | markDirection | ||
// C# — Packing gapMode int packed = (int)gapMode | (asSDF ? 64 : 0) // bit 6 | (invert ? 128 : 0); // bit 7 // HLSL — Unpacking gapMode int gapModeVal = shape.gapMode & 0x3F; // bits 0-5 bool asSDF = (shape.gapMode & 64) != 0; // bit 6 bool invert = (shape.gapMode & 128) != 0; // bit 7 // C# — Packing gapShapeType for LineBurst int packed = cutoutShape | (lineBurstRadial ? 256 : 0); // bit 8 // C# — Packing gapShapeType for Stadia int packed = markDirection | (capStyle << 2) | (symmetryMode << 4);
GPU structured buffers require 16-byte alignment on most platforms. The ShapeData struct is 144 bytes (9 × 16), which satisfies this constraint. If you ever add fields, ensure the total remains a multiple of 16 by adjusting the padding field. Misalignment causes silent data corruption that manifests as randomly flickering shapes.
The ReticleSDF.shader is the heart of the rendering system. It contains two SubShaders that target each supported render pipeline, with all SDF evaluation logic shared in a common include file.
| SubShader | Tags | Usage |
|---|---|---|
| SubShader 0 | "RenderPipeline" = "UniversalPipeline" | Selected by URP. Uses URP vertex/fragment macros and render state. |
| SubShader 1 | "RenderPipeline" = "HDRenderPipeline" | Selected by HDRP. Uses HDRP includes and custom pass conventions. |
Both SubShaders include ReticleSDFCore.hlsl, which contains all shared logic:
StructuredBuffer<ShapeData> _ShapeBuffer; int _ShapeCount; Texture2DArray _CustomSDFArray; half4 FragReticle(Varyings input) : SV_Target { float2 uv = input.uv; half4 result = 0; for (int i = 0; i < _ShapeCount; i++) { ShapeData shape = _ShapeBuffer[i]; // Early-out for invisible shapes if (shape.color.a <= 0 && shape.glowIntensity <= 0) continue; // 1. Transform UV into shape-local space float2 localUV = TransformToLocal(uv, shape); // 2. Evaluate SDF for this shape type float dist = EvaluateSDF(localUV, shape); // 3. Apply gap mode (alpha mask or SDF boolean) dist = ApplyGaps(dist, localUV, shape); // 4. Boolean group compositing dist = ApplyBooleanGroup(dist, i, shape); // 5. Glow (exponential falloff from boundary) result += EvaluateGlow(dist, shape); // 6. Outline (band outside shape boundary) result += EvaluateOutline(dist, shape); // 7. Fill with feathering (fwidth-based AA) result = ComposeFill(result, dist, shape); } return result; }
The EvaluateSDF() function dispatches to the appropriate SDF primitive based on shapeType:
| Shape Type | SDF Function | Characteristic |
|---|---|---|
| Circle (0) | sdCircle(p, r) | Simple length(p) - r |
| Ring (1) | sdCircle + hollow | Circle SDF with abs(d) - thickness hollowing |
| Box (2) | sdBox(p, b) | Axis-aligned signed distance |
| RoundedBox (3) | sdRoundedBox(p, b, r) | Box with corner radius parameter |
| LineBurst (4) | sdLineBurst(p, ...) | N lines/rays with taper, through-center or radial distribution |
| Chevron (5) | sdChevron(p, angle) | V-shape with configurable opening angle |
| Polygon (6) | sdPolygon(p, n, r) | Regular 3–8 sided polygon with corner radius |
| Stadia (8) | sdStadia(p, ...) | Ranging marks: main line + major/minor ticks + caps |
| ChamferBox (9) | sdChamferBox(p, b, c) | Box with beveled corners (octagonal shape) |
| Custom (100) | Texture2DArray sample | Pre-computed Bézier SDF from _CustomSDFArray |
All shapes use fwidth() for screen-space adaptive feathering. This intrinsic returns the sum of partial derivatives of the SDF distance in screen space, providing a pixel-width measurement that produces uniform edge quality regardless of shape size or camera distance. When feather is set to 0, the AA pass is bypassed entirely for pixel-perfect hard edges — useful for thin crosshair lines.
Custom freeform shapes (ShapeType 100) use a multi-stage compute shader pipeline to generate exact signed distance fields from cubic Bézier curves. Unlike analytical shapes that are evaluated per-fragment in real time, custom shapes are pre-computed into a texture and sampled at render time.
| Stage | What Happens |
|---|---|
| Control Points | User-defined CustomControlPoint list with position, handleIn, handleOut, and point type (Corner/Smooth/Symmetric). Edited interactively in the visual editor. |
| Cubic Béziers | Adjacent control points form cubic Bézier curves. The curve array is uploaded to a StructuredBuffer for the compute shader. |
| ExactSDF Kernel | Per-texel, finds the nearest point on the nearest Bézier curve. Uses a coarse search (16 subdivisions per curve) followed by Newton-Raphson refinement (4 iterations) for sub-pixel accuracy. Outputs: R = signed distance, G = second-nearest distance (non-adjacent curves only, for concave glow), A = raw analytical curvature (κ = |x'y'' − y'x''| / |v'|³). |
| BlurCurvature Kernel | Gaussian-smooths the raw curvature from channel A into channel B. Uses an 11×11 grid at 16-texel spacing (σ ≈ 48 texels) to erase sharp Voronoi boundary seams between curve neighborhoods. The smoothed curvature drives glow tapering in the fragment shader. |
| Texture2DArray | Final output: 1024² ARGBHalf texture per custom shape, stored in a Texture2DArray with up to 16 slots. CustomSDFGenerator orchestrates the pipeline, manages slot allocation, and dispatches both kernels sequentially. |
| Channel | Content | Used For |
|---|---|---|
| R | Signed distance to nearest boundary | Fill, outline, feathering, boolean ops — all standard SDF operations |
| G | Second-nearest distance (d2), non-adjacent curves only | Concave glow: light pools in tight concavities where two distant boundary segments are close |
| B | Gaussian-smoothed curvature | Glow tapering: glow *= exp(−κ × glowDist × 1.5) attenuates glow at sharp convex tips (e.g. crescent ends) |
| A | Raw analytical curvature | Input to the BlurCurvature kernel; not used directly in the fragment shader |
The compute shader runs once when control points change — not per frame. At runtime, custom shapes are a single texture sample per fragment, which is actually cheaper than the ALU-heavy analytical SDF evaluation used by standard shapes. The tradeoff is the fixed 8 MB memory cost per custom shape slot (1024² × ARGBHalf).
RCE's runtime contains zero FindFirstObjectByType calls. All component references are resolved through three deterministic patterns, chosen based on the reference context:
Scene-assigned references via [SerializeField]. Used when both the referencing component and the target live in the same scene hierarchy.
public class RCEEditorController : MonoBehaviour { [SerializeField] private ReticleRenderer _reticleRenderer; [SerializeField] private GridRenderer _gridRenderer; [SerializeField] private LayerConnectorRenderer _layerConnectorRenderer; [SerializeField] private CategoryTransitionAnimator _transitionAnimator; [SerializeField] private TracerManager _tracerManager; }
This is the preferred pattern. References are validated at scene save time, are visible in the Inspector, and have zero runtime cost.
Singleton-style Instance properties for components that need to be accessed by ScriptableRendererFeature subclasses, which cannot hold scene references because they live on asset objects.
public class GridRenderer : MonoBehaviour { public static GridRenderer Instance { get; private set; } void OnEnable() => Instance = this; void OnDisable() => Instance = null; } // Used by RCERendererFeature (ScriptableRendererFeature) var grid = GridRenderer.Instance; if (grid != null && grid.IsVisible) grid.EnqueuePass(context);
Components using this pattern: GridRenderer.Instance, LayerConnectorRenderer.Instance, CustomShapeOverlayRenderer.Instance, and ReticleRenderer.Instances (a list, since multiple renderers can exist for main + Global FX).
On-demand component discovery via TryGetComponent with AddComponent fallback. Used for components that are lazily created or optionally present.
// Resolve transition animator from the same GameObject if (!TryGetComponent(out _transitionAnimator)) _transitionAnimator = null; // optional, transitions disabled // Resolve or create shape property animator var rendererGO = _reticleRenderer.gameObject; if (!rendererGO.TryGetComponent(out _shapePropertyAnimator)) _shapePropertyAnimator = rendererGO.AddComponent<ShapePropertyAnimator>(); // On-demand custom shape overlay (only when editing custom shapes) _customShapeOverlayRenderer = gameObject.AddComponent<CustomShapeOverlayRenderer>();
FindFirstObjectByType performs a linear scan of all loaded objects every call. In a game with thousands of GameObjects, this is measurably expensive and non-deterministic (the "first" object depends on internal load order). RCE's three patterns — serialized, static, and runtime resolution — are all O(1) and produce deterministic, inspector-visible results.