CCSCRT.cs

Post-process effect component that emulates a CRT screen look. It exposes user-configurable properties (mask preset, softness, flicker, scanlines, white enhancement, priority), prepares weighted values for blending, sets shader attributes, loads a postprocess shader, and issues a blit pass.

Native Interop
using Sandbox;
using System;
using Sandbox.Rendering;


/// <summary>
/// Emulate the look of a CRT screen
/// </summary>
/// <summary>
/// Emulate the look of a CRT screen
/// </summary>
[Title( "CRT Emulation" )]
[Category( "Post Processing" )]
[Icon( "live_tv" )]
public sealed class CCSCRT : BasePostProcess<CCSCRT>
{
    /// <summary>
    /// Pattern to use for the CRT Mask.
    /// </summary>
    [Property, Title("CRT Mask Preset")]
    public CRTPreset CRTPresetSelection { get; set; } = CRTPreset.SubPixelApertureGrille;
    
    public enum CRTPreset
    {
        [Description( "Vertical Pattern" )]
        SubPixelApertureGrille,
        [Description( "Alternating pattern, looks like shadow mask." )]
        SubPixelShadowMask,
        [Description( "Fake slot-mask pattern" )]
        SubPixelSlotMask,
        [Description( "RGB mask with black lines" )]
        FullPixelGrille,
        [Description( "RGB alternating pattern" )]
        FullPixelShadowMask,
        None
    }
    
    /// <summary>
    /// Blur the image slightly to create a "bloomy" look (works best with white enhance).
    /// </summary>
    [Property, Title("Softness"), Range(0.0f, 10.0f, 0, true)]
    public float bLevel { get; set; } = 1.0f;
    
    /// <summary>
    /// FLASHING LIGHTS WARNING! Enables Screen Flicker (defaults are sane).
    /// </summary>	
    [Property, ToggleGroup("Flicker", Label = "Screen Flicker")]
    public bool Flicker { get; set; }

    /// <summary>
    /// How much to flash, 1 = fully black(crazy). 0.02 = semi realistic.
    /// </summary>
    [Property, Title("Flicker Opacity"), Group("Flicker"), Range(0.0f, 1.0f, 0, true)]
    public float fOpacity { get; set; } = 0.02f;
    
    /// <summary>
    /// How often the screen flickers. Gets weird at high values.
    /// </summary>
    [Property, Title("Flicker Rate"), Group("Flicker"), Range(0.0f, 100.0f, 0, true)]
    public float fRate { get; set; } = 60.0f;
    
    /// <summary>
    /// Enable CRT Scan line emulation.
    /// </summary>	
    [Property, ToggleGroup("ScanLines", Label = "Scan Lines")]
    public bool ScanLines { get; set; }

    /// <summary>
    /// Not the actual number of lines on the screen, but a value which scales the number of lines.
    /// </summary>
    [Property, Title("Line Count"), Group("ScanLines"), Range(0.0f, 1000.0f, 1, true)]
    public float slCount { get; set; } = 480.0f;
    
    /// <summary>
    /// How dark the scanlines are.
    /// </summary>
    [Property, Title("Line Opacity"), Group("ScanLines"), Range(0.0f, 1.0f, 0, true)]
    public float slOpacity { get; set; } = 0.1f;
    
    /// <summary>
    /// Rate at which to scroll the scanlines up or down the screen. high speed + opacity = FLASHING LIGHTS!
    /// </summary>
    [Property, Title("Scroll Rate"), Group("ScanLines"), Range(-100.0f, 100.0f, 0, true)]
    public float slScroll { get; set; } = 1.2f;
    
    /// <summary>
    /// brings back some brighter colors, but some of the crt effect is lost.
    /// </summary>	
    [Property, Title("Enhance Whites")]
    public bool rWhite { get; set; }
    
    /// <summary>
    /// Ensures the effect is applied AFTER others in the stack.
    /// </summary>
    [Property, Title("Low Priority")]  
    public bool fPriority { get; set; }

    public override void Render()
    {
        // ИСПРАВЛЕНО: Для bool и enum берем значения НАПРЯМУЮ, GetWeighted вызывает NullReferenceException!
        CRTPreset currentPreset = CRTPresetSelection;
        
        // Если выбран None, выходим
        if (currentPreset == CRTPreset.None)
            return;
        
        // Числовые значения получаем через GetWeighted (для поддержки PostProcess Volume блендинга)
        float bLevelW = GetWeighted(x => x.bLevel);
        float fOpacityW = GetWeighted(x => x.fOpacity);
        float fRateW = GetWeighted(x => x.fRate);
        float slCountW = GetWeighted(x => x.slCount);
        float slOpacityW = GetWeighted(x => x.slOpacity);
        float slScrollW = GetWeighted(x => x.slScroll);
        
        // Булевые значения берем напрямую
        bool flickerW = Flicker;
        bool scanLinesW = ScanLines;
        bool rWhiteW = rWhite;
        bool priorityW = fPriority;
        
        // Передаем значения в шейдер
        // В s&box bool в шейдер часто передается как int (0 или 1) для надежности
        Attributes.Set("ScanLines", scanLinesW ? 1 : 0);
        Attributes.Set("slCount", slCountW);
        Attributes.Set("slOpacity", slOpacityW);
        Attributes.Set("slScroll", slScrollW);
        
        Attributes.Set("Flicker", flickerW ? 1 : 0);
        Attributes.Set("fOpacity", fOpacityW);
        Attributes.Set("fRate", fRateW);
        
        Attributes.Set("rWhite", rWhiteW ? 1 : 0);
        Attributes.Set("bLevel", bLevelW);
        
        Attributes.Set("style", (int)currentPreset);
        
        // Загружаем шейдер с проверкой на null
        var shader = Material.FromShader("postprocess/ccs_crt.shader");
        if (shader == null)
        {
            Log.Error("CCSCRT: Shader 'postprocess/ccs_crt.shader' not found!");
            return;
        }
        
        // Выбираем приоритет
        int priority = priorityW ? 200 : 2000;
        
        var blit = BlitMode.WithBackbuffer(shader, Stage.AfterPostProcess, priority, true);
        Blit(blit, "CCSCRT");
    }
}