Windows Terminal 像素着色器实战:experimental.pixelShaderPath 与 HLSL 着色器的完整编写、配置与动画指南
本文基于 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):
- 优先级:一旦指定了
experimental.pixelShaderPath,Terminal 会用它替代内建的experimental.retroTerminalEffect(retro 效果同样作用于失焦场景,见 doc/cascadia/profiles.schema.json)。 - 生效时机: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.cpp 与 src/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;
}
操作步骤(照做即可):
- 把上面的代码保存为
C:\temp\invert.hlsl; - 在终端设置的某个 profile 中更新配置:
"experimental.pixelShaderPath": "C:\\temp\\invert.hlsl"
注意 JSON 中路径的反斜杠必须写成双反斜杠转义;
- 保存 settings 文件后,用该 profile 打开一个新终端,屏幕颜色即被反转。
| 左侧:默认终端 | 右侧:应用 Invert 着色器 |
|---|---|
编译失败与重新加载机制
如果着色器编译失败,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)一行就完成了"把终端图形合成到自定义背景上"——这也是你写背景替换类着色器的通用套路(先用Time、tex等算出背景色,再按 alpha 把原画面叠上去)。- 偏移采样制造投影:
tex + 2.0*Scale*float2(-1.0,-1.0)/Resolution.y在 UV 空间向左上偏移"2 个像素"。除以Resolution.y把像素数换算成 UV 距离,乘以Scale抵消高 DPI 缩放,这正是PixelShaderSettings中Scale与Resolution的用途。偏移位置采样到的 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 行的辉光边界。
重载后,你会看到背景出现复古栅格条,且文字带投影、依旧清晰:
内建 Retro 效果的源码剖析:高斯模糊 + 方波扫描线
再复杂一档的例子是 Terminal 内建的 experimental.retroTerminalEffect,其完整实现即本目录的 samples/PixelShaders/Retro.hlsl,并且与编译进 D3D 渲染引擎的内建版本 src/renderer/atlas/custom_shader_ps.hlsl 逐行一致——直接阅读引擎源码即可印证其行为。
其 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;
}
- 辉光模糊(
Blur):对当前纹素周围13×13(sampleCount = 13)的邻域逐点采样,每个样本乘以二维高斯权重Gaussian2D(dx, dy, sigma)(sigma 取2.0f * scale,即随 UI 缩放自适应)。169 次带权采样叠加后以 30% 强度加回原色——这就是 CRT 磷光"泛光"的来源。从源码结构看,这里用的是固定循环次数的朴素卷积,而非多级 mipmap 或双线性近似,属于典型的"GPU 算力换画质"写法。 - 扫描线(
Scanline/SquareWave):SquareWave(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(即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 两级均受支持、为字符串类型的文件路径、且明确标注为实验性特性。
实践清单与注意事项
- 配置:
"experimental.pixelShaderPath": "C:\\path\\to\\shader.hlsl",JSON 内反斜杠需双写;可放在 profile 内按配置定制,优先级高于experimental.retroTerminalEffect。 - 生效时机:按 Schema 描述,着色器在终端失焦(unfocused)时应用;失焦即见效果,聚焦恢复原画面。
- 重载:着色器编译失败会弹警告并临时忽略;修复后重新 touch
settings.json或开新标签页即可重试,无需重启。 - 编写约束:HLSL 无动态内存分配、无指针、无递归;所有画面信息只能来自
shaderTexture采样,所有窗口信息来自PixelShaderSettings(Time/Scale/Resolution/Background)。 - 合成套路:自定义背景类着色器 = "算出背景色 →
lerp(背景, 采样色, 采样色.w)";做像素偏移一律用像素数 * Scale / Resolution.y换算到 UV 空间。 - 调试手段:拿 Nop.hlsl 确认管线、拿 Broken.hlsl 验证错误降级路径,再逐步加入自己的逻辑。
从"12 行反色"到"169 样本高斯辉光",这套机制把终端每一帧都变成了一张可任意二次加工的纹理。以上示例均可在仓库中直接对照源码阅读,动手改参数是最快的学习方式——欢迎基于这些起点继续实验。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00