首页
/ Windows Terminal 像素着色器实战:experimental.pixelShaderPath 与 HLSL 着色器的完整编写、配置与动画指南

Windows Terminal 像素着色器实战:experimental.pixelShaderPath 与 HLSL 着色器的完整编写、配置与动画指南

2026-09-06 14:53:05作者:傅爽业Veleda

本文基于 Windows Terminal(Windows Terminal and the original Windows console host)官方示例文档 samples/PixelShaders/README.md 整理并深度扩充,完整讲解如何通过 experimental.pixelShaderPath 设置项为终端加载自定义 HLSL 像素着色器:从最小反转着色器、扫描线/栅格条特效、内建复古效果的源码剖析,到基于 Time 变量的一维滚动与往返呼吸动画。读完后你可以独立编写、配置并调试自己的终端像素着色器,理解终端画面是如何被送入 GPU 纹理、再由着色器逐像素重采样的。

特性总览:一个实验性设置项把终端画面交给 GPU

GPU 拥有巨大的并行算力,像素着色器可以实时完成分形缩放、光线追踪、图像处理等运算。Windows Terminal 允许用户提供一个像素着色器文件,由渲染引擎将其编译并应用于终端画面。启用方式是在任意一个 profile(或全局 settings 段)中加入:

"experimental.pixelShaderPath": "<path to a .hlsl pixel shader>"

两个关键行为,均有官方 JSON Schema 佐证(见 doc/cascadia/profiles.schema.json):

  1. 优先级:一旦指定了 experimental.pixelShaderPath,Terminal 会用它替代内建的 experimental.retroTerminalEffect(retro 效果同样作用于失焦场景,见 doc/cascadia/profiles.schema.json)。
  2. 生效时机:Schema 中对该字段的描述是 "Use to set a path to a pixel shader to use with the Terminal when unfocused",即着色器在终端失去焦点时应用——这与内建复古效果"失焦时才显示"的行为一致。该字段为实验性特性,官方注明"其后续存续不做保证"("This is an experimental feature, and its continued existence is not guaranteed")。

该字段同时出现在 profile 级别与全局 settings 级别(doc/cascadia/profiles.schema.json),因此可以按 profile 定制各自的着色器。从源码结构看,该设置经由 src/cascadia/TerminalSettingsModel/MTSMSettings.h 进入设置模型,由 src/cascadia/ControlProperties.h 传给终端控件,最终在 src/renderer/atlas/AtlasEngine.api.cppsrc/renderer/atlas/BackendD3D.cpp 的 D3D 后端中被加载与绑定——终端文本被先渲染成一张纹理,着色器再对这张纹理做逐像素变换。内建 retro 着色器本身也以源文件形式随引擎编译:src/renderer/atlas/custom_shader_ps.hlsl,与下文示例 samples/PixelShaders/Retro.hlsl 内容一致。

快速上手:12 行的颜色反转着色器

官方示例目录中最简单的起点是 samples/PixelShaders/Invert.hlsl。完整代码如下:

// A minimal pixel shader that inverts the colors

// The terminal graphics as a texture
Texture2D shaderTexture;
SamplerState samplerState;

// Terminal settings such as the resolution of the texture
cbuffer PixelShaderSettings {
  // The number of seconds since the pixel shader was enabled
  float  Time;
  // UI Scale
  float  Scale;
  // Resolution of the shaderTexture
  float2 Resolution;
  // Background color as rgba
  float4 Background;
};

// A pixel shader is a program that given a texture coordinate (tex) produces a color.
// tex is an x,y tuple that ranges from 0,0 (top left) to 1,1 (bottom right).
// Just ignore the pos parameter.
float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET
{
    // Read the color value at the current texture coordinate (tex)
    //  float4 is tuple of 4 floats, rgba
    float4 color = shaderTexture.Sample(samplerState, tex);

    // Inverts the rgb values (xyz) but don't touch the alpha (w)
    color.xyz = 1.0 - color.xyz;

    // Return the final color
    return color;
}

操作步骤(照做即可):

  1. 把上面的代码保存为 C:\temp\invert.hlsl
  2. 在终端设置的某个 profile 中更新配置:
"experimental.pixelShaderPath": "C:\\temp\\invert.hlsl"

注意 JSON 中路径的反斜杠必须写成双反斜杠转义;

  1. 保存 settings 文件后,用该 profile 打开一个新终端,屏幕颜色即被反转。

Invert 着色器效果:左为默认终端,右为像素级反色后的终端

左侧:默认终端 右侧:应用 Invert 着色器
Default Terminal Inverted Terminal

编译失败与重新加载机制

如果着色器编译失败,Terminal 会弹出一个警告对话框并临时忽略该着色器。修复后,重新 touch 一下 settings.json 文件,或者直接打开一个新标签页,Terminal 就会再次尝试加载着色器——无需重启整个应用。

着色器 API 契约:终端向 HLSL 暴露了什么

所有终端像素着色器共享同一套全局约定(见 samples/PixelShaders/README.md):

全局符号 类型 含义
shaderTexture Texture2D 终端当前画面(文本、背景)作为纹理。这是你唯一"输入画面"的来源,必须通过它采样
samplerState SamplerState 配套的采样器,shaderTexture.Sample(samplerState, uv) 的标准搭档
PixelShaderSettings.Time float 着色器启用后经过的秒数,只能递增,驱动动画的核心变量
PixelShaderSettings.Scale float UI 缩放比例。做"按像素偏移"的运算时要乘它以适配 DPI
PixelShaderSettings.Resolution float2 shaderTexture 的像素分辨率(宽, 高)。1.0/Resolution.y 即单行像素在 UV 空间的高度
PixelShaderSettings.Background float4 终端背景色(rgba)

入口函数签名为 float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET:着色器是一个"给定纹理坐标 tex 就产出一个颜色"的纯函数。tex 是 0,0(左上角)到 1,1(右下角)的归一化坐标;pos 参数可以直接忽略。

关于 HLSL 语言本身:它是一种类 C 语言,但有若干限制——不能动态分配内存、不能使用指针、不能递归。作为交换,你能得到体感为 teraflop 量级的并行算力,实时光线追踪、分形等效果在近年 GPU 上完全可行。社区中大量 GLSL 写的像素着色器案例(如 Shadertoy 上的 menger sponge 等分形/光线追踪作品)在熟悉之后可以较容易地移植为 HLSL。

进阶示例:Retro 栅格条 + 文字投影(Rasterbars)

接下来看官方给出的更复杂示例——80 年代 CRT 风格的栅格条(raster bars)背景,源码为 samples/PixelShaders/Rasterbars.hlsl

// A minimal pixel shader that shows some raster bars

// The terminal graphics as a texture
Texture2D shaderTexture;
SamplerState samplerState;

// Terminal settings such as the resolution of the texture
cbuffer PixelShaderSettings {
  // The number of seconds since the pixel shader was enabled
  float  Time;
  // UI Scale
  float  Scale;
  // Resolution of the shaderTexture
  float2 Resolution;
  // Background color as rgba
  float4 Background;
};

// A pixel shader is a program that given a texture coordinate (tex) produces a color.
// tex is an x,y tuple that ranges from 0,0 (top left) to 1,1 (bottom right).
// Just ignore the pos parameter.
float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET
{
    // Read the color value at the current texture coordinate (tex)
    //  float4 is tuple of 4 floats, rgba
    float4 color = shaderTexture.Sample(samplerState, tex);

    // Read the color value at some offset, will be used as shadow
    float4 ocolor = shaderTexture.Sample(samplerState, tex+2.0*Scale*float2(-1.0, -1.0)/Resolution.y);

    // Thickness of raster
    const float thickness = 0.1;

    float ny = floor(tex.y/thickness);
    float my = tex.y%thickness;
    const float pi = 3.141592654;


    // ny is used to compute the rasterbar base color
    float cola = ny*2.0*pi;
    float3 col = 0.75+0.25*float3(sin(cola*0.111), sin(cola*0.222), sin(cola*0.333));

    // my is used to compute the rasterbar brightness
    //  smoothstep is a great little function: https://en.wikipedia.org/wiki/Smoothstep
    float brightness = 1.0-smoothstep(0.0, thickness*0.5, abs(my - 0.5*thickness));

    float3 rasterColor = col*brightness;

    // lerp(x, y, a) is another very useful function: https://en.wikipedia.org/wiki/Linear_interpolation
    float3 final = rasterColor;
    // Create the drop shadow of the terminal graphics
    //  .w is the alpha channel, 0 is fully transparent and 1 is fully opaque
    final = lerp(final, float(0.0), ocolor.w);
    // Draw the terminal graphics
    final = lerp(final, color.xyz, color.w);

    // Return the final color, set alpha to 1 (ie opaque)
    return float4(final, 1.0);
}

这段代码是理解"终端画面 = 带 alpha 的纹理"这一关键模型的最佳范本:

  • color.w 就是字形不透明度。终端画面纹理中,背景区域 alpha 接近 0,文字像素 alpha 接近 1。因此 final = lerp(final, color.xyz, color.w) 一行就完成了"把终端图形合成到自定义背景上"——这也是你写背景替换类着色器的通用套路(先用 Timetex 等算出背景色,再按 alpha 把原画面叠上去)。
  • 偏移采样制造投影tex + 2.0*Scale*float2(-1.0,-1.0)/Resolution.y 在 UV 空间向左上偏移"2 个像素"。除以 Resolution.y 把像素数换算成 UV 距离,乘以 Scale 抵消高 DPI 缩放,这正是 PixelShaderSettingsScaleResolution 的用途。偏移位置采样到的 alpha(ocolor.w)被用来先把目标色拉黑,形成文字左上方 45° 的落影,再叠回正文,提升栅格条背景上的可读性。
  • 条带的基色ny = floor(tex.y/thickness) 把垂直方向按厚度 0.1 切成横条索引,用三个频率错开的 sin 生成每条不同的底色;
  • 条带亮度包络my = tex.y%thickness 是条内局部坐标,1.0 - smoothstep(0.0, thickness*0.5, abs(my - 0.5*thickness)) 让亮度在条中心最亮、向边缘平滑衰减,模拟 CRT 行的辉光边界。

重载后,你会看到背景出现复古栅格条,且文字带投影、依旧清晰:

Rasterbars 着色器效果:彩色栅格条背景上的终端输出,文字带左上角投影

内建 Retro 效果的源码剖析:高斯模糊 + 方波扫描线

再复杂一档的例子是 Terminal 内建的 experimental.retroTerminalEffect,其完整实现即本目录的 samples/PixelShaders/Retro.hlsl,并且与编译进 D3D 渲染引擎的内建版本 src/renderer/atlas/custom_shader_ps.hlsl 逐行一致——直接阅读引擎源码即可印证其行为。

Retro 着色器效果:失焦时终端呈现扫描线与辉光模糊的复古 CRT 观感

main 函数只有三步:

float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET
{
    // TODO:GH#3930 Make these configurable in some way.
    float4 color = shaderTexture.Sample(samplerState, tex);
    color += Blur(shaderTexture, tex, SCALED_GAUSSIAN_SIGMA) * 0.3f;
    color = Scanline(color, pos);

    return color;
}
  1. 辉光模糊(Blur:对当前纹素周围 13×13sampleCount = 13)的邻域逐点采样,每个样本乘以二维高斯权重 Gaussian2D(dx, dy, sigma)(sigma 取 2.0f * scale,即随 UI 缩放自适应)。169 次带权采样叠加后以 30% 强度加回原色——这就是 CRT 磷光"泛光"的来源。从源码结构看,这里用的是固定循环次数的朴素卷积,而非多级 mipmap 或双线性近似,属于典型的"GPU 算力换画质"写法。
  2. 扫描线(Scanline / SquareWaveSquareWave(y) 返回 1.0f - (floor(y / SCALED_SCANLINE_PERIOD) % 2.0f) * SCANLINE_FACTOR,即周期为 scale、占空比 50% 的方波(SCANLINE_FACTOR 0.5f),最终 color * wave 让隔行亮度减半,形成细密扫描线。源码中还留有一处被 && false 关闭的实验分支(标注 TODO:GH#3929),意图是让扫描线只在暗背景上加亮而非全局乘暗,说明该效果仍在迭代中。

这两处 TODO(GH#3929 / GH#3930)也解释了为何官方推荐自定义 pixelShaderPath:内建效果的强度、周期尚未开放配置,想要"只要辉光不要扫描线"之类的变体,最直接的途径就是复制 Retro.hlsl 改参数。

动画效果一:用 Time 驱动单向滚动扫描线

Time(着色器加载后的秒数)是驱动动画的输入。官方示例 samples/PixelShaders/Animate_scan.hlsl 让一行反色像素从上往下滚动:

float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET
{
    // Read the color value at the current texture coordinate (tex)
    float4 color = shaderTexture.Sample(samplerState, tex);

    // Here we spread the animation over 5 seconds. We use time modulo 5 because we want
    // the timer to count to five repeatedly. We then divide the result by five again
    // to get a value between 0.0 and 1.0, which maps to our texture coordinate.
    float linePosition = Time % 5 / 5;

    // Since TEXCOORD ranges from 0.0 to 1.0, we need to divide 1.0 by the height of the
    // texture to find out the size of a single pixel
    float lineWidth = 1.0 / Resolution.y;

    // If the current texture coordinate is in the range of our line on the Y axis:
    if (tex.y > linePosition - lineWidth && tex.y < linePosition)
    {
        // Invert the sampled color
        color.rgb = 1.0 - color.rgb;
    }

    return color;
}

两个要点:

  • 周期归一化Time % 5 / 5 把只增不减的时间映射回 [0.0, 1.0),即"每 5 秒完整走一遍纹理",正好对应 UV 坐标域;
  • 像素级线宽:UV 空间里一个物理像素的高度是 1.0 / Resolution.y,用它判定 tex.y 是否落在扫描线上,可保证线宽恒为 1 行像素、不随窗口大小变化。

动画效果二:余弦调制实现"往返呼吸"背景

如果希望动画往返往复(而非单向循环),Time 只增不减的特性就需要借助三角函数。官方示例 samples/PixelShaders/Animate_breathe.hlsl 让背景在两种颜色之间"呼吸"式渐变。

cos() 输出 [-1.0, 1.0],通用波形整形公式为:

a * cos(b * (x - c)) + d

其中 a 调振幅、b 调波长/频率、c 调 x 轴偏移、d 调 y 轴偏移。可用图形计算器(如 Windows 自带计算器)可视化实验。

余弦波形示例:半波加 0.5 偏移后把 cos 的值域平移到 0.0–1.0

  • 输出减半再加 0.5(即 a = 0.5, d = 0.5),把值域平移到 [0.0, 1.0],正好可以直接作为 lerp 的插值因子;
  • cos() 以弧度为输入:将 x(这里是 Time)乘以 tau(2*pi),等效于把波长设为 1 秒——整个动画周期即为 1 秒;把 tau 除以期望的秒数即可任意调整时长。本例取 5 秒。

完整实现:

// pi and tau (2 * pi) are useful constants when using trigonometric functions
#define TAU 6.28318530718

float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET
{
    // Read the color value at the current texture coordinate (tex)
    float4 sample = shaderTexture.Sample(samplerState, tex);

    // The number of seconds the breathing effect should span
    float duration = 5.0;

    float3 color1 = float3(0.3, 0.0, 0.5);  // indigo
    float3 color2 = float3(0.1, 0.1, 0.44); // midnight blue

    // Set background colour based on the time
    float4 backgroundColor = float4(lerp(color1, color2, 0.5 * cos(TAU / duration * Time) + 0.5), 1.0);

    // Draw the terminal graphics over the background
    return lerp(backgroundColor, sample, sample.w);
}

最后 lerp(backgroundColor, sample, sample.w) 按字形 alpha 把终端图形合成在呼吸背景之上——与 Rasterbars 中"背景 + alpha 合成"的手法完全同构,是写自定义背景类着色器的标准收尾。

示例着色器全览与其他可用素材

samples/PixelShaders 目录中还提供了更多可直接运行的 .hlsl 起点,覆盖了从"验证管线"到"故障演示"的各种场景:

文件 用途
Invert.hlsl 最小示例:整体反色,用于验证着色器链路打通
Nop.hlsl 直通(no-op),采样后原样返回,适合做调试基线
Grayscale.hlsl 灰度化
Outlines.hlsl 边缘/描边效果
BackgroundImage.hlsl 背景图替换类着色器(配合自定义纹理的思路可参考 Rasterbars 的 alpha 合成写法)
Rasterbars.hlsl 彩色栅格条 + 文字投影
Retro.hlsl 内建复古效果的完整实现(高斯辉光 + 扫描线)
Animate_scan.hlsl 单向滚动反色扫描线
Animate_breathe.hlsl 余弦往返的背景呼吸动画
Broken.hlsl 故意写坏的着色器,可用来验证"编译失败→警告对话框→临时忽略"的降级行为
Error.hlsl 编译错误演示,同属调试参考

配合官方 Schema(doc/cascadia/profiles.schema.json)可确认该设置项在 profile 与全局 settings 两级均受支持、为字符串类型的文件路径、且明确标注为实验性特性。

实践清单与注意事项

  1. 配置"experimental.pixelShaderPath": "C:\\path\\to\\shader.hlsl",JSON 内反斜杠需双写;可放在 profile 内按配置定制,优先级高于 experimental.retroTerminalEffect
  2. 生效时机:按 Schema 描述,着色器在终端失焦(unfocused)时应用;失焦即见效果,聚焦恢复原画面。
  3. 重载:着色器编译失败会弹警告并临时忽略;修复后重新 touch settings.json 或开新标签页即可重试,无需重启。
  4. 编写约束:HLSL 无动态内存分配、无指针、无递归;所有画面信息只能来自 shaderTexture 采样,所有窗口信息来自 PixelShaderSettingsTime / Scale / Resolution / Background)。
  5. 合成套路:自定义背景类着色器 = "算出背景色 → lerp(背景, 采样色, 采样色.w)";做像素偏移一律用 像素数 * Scale / Resolution.y 换算到 UV 空间。
  6. 调试手段:拿 Nop.hlsl 确认管线、拿 Broken.hlsl 验证错误降级路径,再逐步加入自己的逻辑。

从"12 行反色"到"169 样本高斯辉光",这套机制把终端每一帧都变成了一张可任意二次加工的纹理。以上示例均可在仓库中直接对照源码阅读,动手改参数是最快的学习方式——欢迎基于这些起点继续实验。

登录后查看全文
热门项目推荐
相关项目推荐