Android 13 RuntimeShader实战:AGSL与RenderEffect实现GPU着色

发布时间:2026/9/17 2:58:56

Android 13 RuntimeShader实战:AGSL与RenderEffect实现GPU着色 简介面向Android图形开发者的RuntimeShader应用示例包围绕运行时着色器在View二次渲染、Canvas渲染、Canvas二次渲染三种典型场景演示逐像素渲染的完整实现思路。逐像素渲染会在每个像素上执行着色操作相比逐顶点方式能更精细地呈现阴影、光照与反射效果适合具备基础图形学知识、希望掌握AGSL和RenderEffect用法的开发者。压缩包共1541个文件涵盖agsl着色器源码、Java业务类、XML布局资源、Gradle构建配置及编译产物目录结构清晰便于按模块索引可直接还原为可运行工程包体约60.85MB。已有115人学习。除三个可交互Demo外还提供关键渲染链路的注释说明可结合配套博客对照分析逐像素操作如何作用在视图与画布上便于快速迁移到自定义特效或图像处理项目中。1. RuntimeShader 是什么为什么值得现在学RuntimeShader 是 Android 13API 33开放给应用层的可编程像素着色能力。以前想在普通 View 上做流光扫过文本、胶片颗粒滚动、玻璃折射扭曲这类实时效果要么上 OpenGL 自己拼整条渲染管线要么靠 RenderScript 顶着停止维护的压力硬撑现在用一个 RuntimeShader 实例加一段 AGSL 源码就能借 RenderEffect 渲染链路把它挂到任意 Paint 上GPU 会在离屏缓冲上逐像素重算再合成回目标画面。它特别适合画面主体仍然是 View 或 Compose、只有局部效果需要定制的场景也适合做主题引擎统一给图标、背景、卡片加一层后处理。下面把原理、两套接入写法、uniform 调参和高频坑依次讲清楚内容默认基于 API 33 及以上读完可以直接落到项目里。2. RuntimeShader 的原理RenderEffect 与 AGSL 各管什么2.1 RuntimeShader 在渲染管线里的位置RuntimeShader 是Shader的子类这一点容易让人误解以为它和BitmapShader、LinearGradient一样只是画刷的一种纹理来源。实际上 RuntimeShader 没有内置的像素生成逻辑它的唯一任务是把 AGSL 源码编译成 GPU 上的片段着色程序。真正让它生效的是RenderEffectRenderEffect.createRuntimeShaderEffect(runtimeShader, inputName)把着色器包装成一种渲染效果再通过Paint.setRenderEffect挂到绘制链路上。系统在执行 Canvas 绘制时先把绘制内容放进一个离屏缓冲区然后运行 AGSL 里的main函数对缓冲区每个像素重新取值最后把结果合成到屏幕上。一个常见的错误认知是把 RuntimeShader 当成Paint.setShader来用直接设置给 paint 再画矩形。Paint.setShader只决定图形内部的填充内容不会触发逐像素的后处理而Paint.setRenderEffect是在 GPU 绘制完之后再做一层像素级加工。把这两者分清之后你对 RuntimeShader 的定位就准确了画上去的东西仍然是原样的只是渲染链路的最后一公里被替换成了着色器输出。需要注意版本边界。RenderEffect 在 API 31 引入RuntimeShader 在 API 33 才开放minSdk 低于 33 时不能直接写死这个类需要做版本判断和降级路径。常见做法是抽一个效果接口低版本回退到 ColorFilter 或直接关闭特效我一般会在项目里把它包在一个EffectLayer里由调用方决定传入 RuntimeShader 还是 nullval effect: RenderEffect? if (Build.VERSION.SDK_INT 33) { RenderEffect.createRuntimeShaderEffect(runtimeShader, null) } else { null } paint.renderEffect effect if (Build.VERSION.SDK_INT 33) { // 低版本关闭效果保证功能仍然可用 }这段代码里的null是输入着色器名表示当前效果不需要读取原画面直接从零生成颜色。后面 2.2 会讲到输入纹理的用法。2.2 AGSL 的入口函数、坐标约定与输入着色器AGSL 全称 Android Graphics Shading Language语法兼容 GLSL ES 的一个子集但入口函数固定。每个像素都会执行一次main参数fragCoord是当前像素在缓冲区里的坐标起点在左上角横向右、纵向下的方向增大单位是像素而不是归一化的 01。这一点和很多 Web 端的 shader 工具完全不同写第一段 AGSL 时最容易在这里栽跟头。一个最基础的 RuntimeShader 源码长这样uniform float2 uSize; uniform float uTime; half4 main(float2 fragCoord) { float2 uv fragCoord / uSize; float3 col 0.5 0.5 * cos(uTime * 0.6 uv.xyx vec3(0.0, 2.0, 4.0)); return half4(col, 1.0); }注意half4是输出类型表示 RGBA 四通道其中half是半精度浮点。GPU 上处理半精度比 float 快颜色计算场景精度足够这也是官方示例里大量使用half4而少用vec4的原因。uv.xyx是在组合向量展开成(uv.x, uv.y, uv.x)最终得到三个浮点分量分别作为 RGB 的波动相位。uSize 和 uTime 都是外部注入的 uniform前者告诉着色器缓冲区尺寸后者驱动动画。如果要做读取原画面再处理需要动用输入着色器。在 AGSL 里声明一个uniform shader uInput;然后在main里对输入做evaluniform shader uInput; half4 main(float2 fragCoord) { half4 src uInput.eval(fragCoord); return half4(1.0 - src.rgb, src.a); }这段代码实现了最简单的反色效果。uInput 这个名称不是随便起的它必须和RenderEffect.createRuntimeShaderEffect(runtimeShader, uInput)的第二个参数严格对应系统才会把当前 Paint 输入给 Canvas 的内容绑定到这个 uniform 变量上。2.3 uniform 类型与命名约束AGSL 里的 uniform 变量命名有约定。我建议统一加上u前缀这样setFloatUniform在匹配变量名时不容易和系统内部自动注入的坐标、尺寸等变量撞名。你在setFloatUniform里填的名字必须和 AGSL 源码中的变量名一字不差否则运行时会抛出IllegalArgumentException。类型也很严格float 用对应浮点重载float2 要按顺序传两个数float4 或颜色可以用专门的颜色注入方法。常见类型映射关系AGSL 类型RuntimeShader 方法示例用途floatsetFloatUniform(String, float)时间、强度、进度float2setFloatUniform(String, float, float)尺寸、偏移量float3/float4setColorUniform(String, Color)注入颜色值mat3/mat4setFloatUniform(String, FloatArray)矩阵变换shadercreateRuntimeShaderEffect(shader, 名字)输入纹理绑定这个表基本覆盖了日常写效果会用到的输入类型。setFloatUniform也有一些接收FloatArray的重载传数组时保证数组长度和 AGSL 声明的类型一致顺序不能乱。第 3 章和第 4 章的代码都会围绕这些方法展开。3. 在自定义 View 与 Compose 中接入 RuntimeShader 的落地代码3.1 在自定义 View 里挂 RuntimeShader 的最小代码先实现一个动态胶片颗粒效果也就是每个像素叠加一个随时间滚动的随机值。在自定义 View 里分创建着色器、挂载效果、更新 uniform、绘制四步class GrainView JvmOverloads constructor( context: Context, attrs: AttributeSet? null, ) : View(context, attrs) { private val runtimeShader RuntimeShader( uniform float2 uSize; uniform float uTime; half4 main(float2 fragCoord) { float2 uv fragCoord / uSize; float grain fract(sin(dot(uv uTime, vec2(12.9898, 78.233))) * 43758.5453); return half4(vec3(grain), 1.0); } .trimIndent() ) private val paint Paint().apply { renderEffect RenderEffect.createRuntimeShaderEffect(runtimeShader, null) } override fun onAttachedToWindow() { super.onAttachedToWindow() setLayerType(LAYER_TYPE_HARDWARE, null) startAnimator() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() animator?.cancel() } private var animator: ValueAnimator? null private fun startAnimator() { animator ValueAnimator.ofFloat(0f, 100f).apply { duration 4000 repeatCount ValueAnimator.INFINITE addUpdateListener { anim - runtimeShader.setFloatUniform(uTime, anim.animatedValue as Float) invalidate() } start() } } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) runtimeShader.setFloatUniform(uSize, width.toFloat(), height.toFloat()) canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) } }这里最容易被忽略的是setLayerType(LAYER_TYPE_HARDWARE, null)。RuntimeShader 要求硬件加速环境虽然 targetSdk 较高时 View 默认会走硬件渲染但显式指定后可以排除掉模拟器和部分定制 ROM 的兼容性问题。如果没有硬件加速drawRect 不会报编译错误但屏幕上什么都看不到这是排查时要看的第一个位置。参数更新节奏也有讲究uTime 在动画回调里更新uSize 在 onDraw 里更新。为什么不在同一个地方更新因为 onDraw 每次都会执行转屏或尺寸变化时uSize 必须跟着最新坐标系走uTime 是连续增长的放在动画回调里更新可以避免 invalidate 频率和动画频率不一致导致的跳变。3.2 在 Compose 中不经过 AndroidView 直接绘制Compose 的 Canvas 底层仍然走 Android 原生绘制所以 RuntimeShader 可以绕开 Interop 直接嵌入到组合阶段。常见做法是把 RuntimeShader 和 RenderEffect 放进remember再用drawIntoCanvas拿到 nativeCanvas 配置 Paint。不要在 Modifier.drawBehind 里直接操作 RenderEffect那个阶段的画笔不在开发者手里可控性差。正确的入口是drawContext.canvas的原生层Composable fun RuntimeShaderLayer( modifier: Modifier Modifier, agslSource: String, ) { val shader remember { RuntimeShader(agslSource) } val renderEffect remember { RenderEffect.createRuntimeShaderEffect(shader, null) } Canvas(modifier) { shader.setFloatUniform(uSize, size.width, size.height) drawIntoCanvas { canvas - canvas.nativeCanvas.drawRect( 0f, 0f, size.width, size.height, android.graphics.Paint().apply { this.renderEffect renderEffect } ) } } }如果你想驱动动画不要用remember { mutableStateOf }每秒刷新 60 次最好接withFrameNanos让时间参数和渲染帧同步。Shader 和 RenderEffect 都必须在 remember 里缓存如果每次重组都 new 一个 RuntimeShaderGPU 端会反复重建编译资源掉帧非常明显。3.3 用输入纹理做后处理最常用的场景其实是先有内容再加效果而不是从头生成效果。比如给编辑页的图片加马赛克或故障效果输入不是纯色矩形而是 Bitmap 或当前 View 的内容。这种场景要记得给 RuntimeShader 声明输入 uniformuniform shader uInput; uniform float2 uSize; half4 main(float2 fragCoord) { float2 uv fragCoord / uSize; float2 mosaic floor(uv * 30.0) / 30.0; return uInput.eval(mosaic * uSize); }Kotlin 端创建 effect 时要加上输入名val effect RenderEffect.createRuntimeShaderEffect(runtimeShader, uInput)创建后这个 effect 配合同一个 Paint 绘制到 Canvas 上输入就是 Canvas 上画的内容。如果输入来自一张 Bitmap可以先把 BitmapShader 包一层再用RuntimeShader.setInputShader(uInput, bitmapShader)注入这样就不需要先把 Bitmap 画到画布上再做第二步处理省掉一个中间缓冲。接入方式可以参考这个表场景推荐接入原因单个 View 内容加效果onDraw Paint.setRenderEffect最小改动状态都在 View 内Compose 背景或内容特效Canvas drawIntoCanvas不引入 AndroidView跟随重组图片滤镜、相机帧setInputShader 绑 BitmapShader输入可控省一次离屏拷贝第 3.1 和 3.2 两种写法是后续调参的基础第 4 章涉及的 uniform 更新在这两个环境下都适用。4. RuntimeShader 调参uniform 命名、动效与三个高频坑4.1 推荐先调稳这四个 uniform写 RuntimeShader 效果时我一般不会一上来就抠颜色公式而是先把四个最容易引起整体偏差的参数调稳它们是 uSize、uTime、uStrength、uInput。下面这张表列出最常见的初始值范围和作用uniform类型推荐初始值作用uSizefloat2View 实时宽高把像素坐标转成 0~1 的 uvuTimefloat从 0 开始连续递增所有随时间变化的波动uStrengthfloat0.0~1.0控制效果混合强度uInputshader当前画面或 BitmapShader原始画面采样uSize 必须按实际尺寸更新否则转屏后 uv 计算会整体错位。uTime 不需要重置连续增长最适合做周期波动。uStrength 通常配合 mix 函数使用mix 是线性插值源画面和效果画面按比例混合强度为 0 时完全显示原图为 1 时完全显示处理结果uniform shader uInput; uniform float uStrength; half4 main(float2 fragCoord) { half4 src uInput.eval(fragCoord); half4 processed half4(1.0 - src.rgb, src.a); return mix(src, processed, uStrength); }这段 AGSL 里uStrength 设成 0.3 就是 30% 的反色强度。设置方式用一句话就是runtimeShader.setFloatUniform(uStrength, 0.3f)。4.2 动画驱动时 uniform 更新顺序RuntimeShader 的 uniform 不是一改就立刻生效的它要等下一帧渲染时才会被 GPU 读取。所以动画代码里不要出现这种情况先setFloatUniform(uTime, newTime)紧接着去读画面像素你读到的仍然是上一帧的结果。正确顺序是先更新所有 uniform再触发 invalidate最后在下一帧 onDraw 里看到效果。模拟器上做验证经常遇到另一个现象在 API 33 以下模拟器上RuntimeShader 类根本不存在代码在类加载阶段就抛异常。需要在初始化处加版本拦截让低版本设备完全避开这个分支val canUseRuntimeShader Build.VERSION.SDK_INT 33 if (canUseRuntimeShader) { val shader RuntimeShader(source) }这里没有做反射或动态代理因为官方只保证 API 33 以上稳定与其维护兼容层不如在低版本关闭效果节省维护成本。4.3 三个高频踩坑点第一个坑是软渲染静默失效。在模拟器或者关闭硬件加速的 View 上RenderEffect 通常不报错但效果不出现。排查顺序先看 manifest 里 hardwareAccelerated 属性再看 View 的 isHardwareAccelerated 返回值最后确认 setLayerType 设的是 LAYER_TYPE_HARDWARE 而不是 LAYER_TYPE_SOFTWARE。第二个坑是 Canvas 的裁剪和位移影响坐标。如果外层对 Canvas 做了 translate或者 View 有非零的 paddingfragCoord的起点就可能和 uSize 对不上效果出现偏移。处理办法是在 onDraw 里通过canvas.getClipBounds()获取真实渲染区域再把这个区域宽高传成 uSize而不是直接用 View 的宽高。第三个坑是半精度精度问题。GPU 的半精度浮点数对很大的数值不敏感uTime 跑到几千之后sin 和 cos 的结果可能开始出现抖动。所以 uTime 每累计到一定范围后需要做取模比如uTime % 100.0既保留动画连续性又避免精度劣化。很多看起来莫名其妙的闪烁追到最后都是这个原因。注意RuntimeShader 的编译发生在首次绘制那一帧屏幕会多出一帧的停顿正规做法是把必要的着色器在应用启动后台预编译或者把当前页面提前画一帧让编译开销不在用户交互路径上。5. 用网格法快速验证 RuntimeShader 坐标系没有偏移网格法最大优势是不需要断点、不需要日志肉眼观察亮线间距就能判断坐标是否准确。把下面这段 AGSL 挂到任何 View 上正常情况下画面中会出现 12 条等距的竖线从左向右依次排到 11/12 的位置uniform float2 uSize; half4 main(float2 fragCoord) { float2 uv fragCoord / uSize; float line step(0.97, abs(sin(uv.x * 12.0))); return half4(vec3(line), 1.0); }uv.x * 12.0让 sin 在水平方向完成 12 个周期step(0.97, ...)只保留每个周期末段接近 1 的值于是亮线落在每个周期的末尾。验证时用一只手遮住画面的某一个局部区域另一只手看线的间距和数量有没有发生变化。观察结果结论处理方向12 条等距竖线坐标与缓冲尺寸匹配无需处理竖线数量多于 12uSize 大于实际缓冲区改成画布实际宽高竖线数量少于 12uSize 小于实际缓冲区检查窗口 insets 和旋转竖线倾斜或截断Canvas 被 translate 或 clip去掉外层位移或改离屏画布如果只是靠目测判断颜色对不对很容易被屏幕亮度干扰网格法的判断标准是几何位置这是着色器坐标系正确性的第一道防线。要想更精确可以在 View 的 onDraw 里配合 PixelCopy 把渲染结果回读到 Bitmap然后检查某个像素的 RGB 是否落在预期区间首次回读要预留一两帧等待 GPU 完成写回。当你打算把同一个 AGSL 源码复用到多个项目时保存一份只做网格验证的极简 shader排错时先切换成它就能快速区分坐标问题还是颜色计算问题。这个习惯在处理多设备适配时尤其省时间AGSL 在部分 GPU 驱动上的坐标行为不完全一致网格线会第一时间暴露差异。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/17 2:58:56

RH850/F1L RIIC寄存器级I²C驱动详解与实操指南

简介:本资源是面向Renesas RH850/F1L汽车级32位MCU开发者的IC(RIIC)底层驱动工程包,专为嵌入式软件工程师及单片机进阶学习者设计,解决IC主从双向通信的典型实现难题。压缩包共59个文件,含18个C源码&#x…

2026/9/17 2:58:56

Everything Claude Code 跑智能体任务:Key 用 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/17 2:58:56

Excel到Visio自动生成图的三种替代方案:从VBA到Python实战

Excel Data Visualizer 退役这件事,确实打乱了不少人的习惯流程。以前在 Excel 里把流程步骤、负责人、前后关系填好,点一下加载项,Visio 图就自动出来了,连配色和泳道都帮你排好;现在这条路已经走不通,微软…

2026/9/17 3:59:00

2026年9月MacBook选购指南:M3/M3 Pro实测与VMware/ENSP适配方案

1. 为什么2026年9月这个时间点买MacBook,必须重新算一笔账?2026年9月不是普通的时间节点——它恰好卡在苹果芯片代际更迭的“静默期”与“爆发前夜”之间。我从2015年开始做Mac生态适配服务,经手过超过3700台Mac设备的选型、部署和故障排查&a…

2026/9/17 3:58:59

Linux 字体推荐与字体冲突排查:终端中文显示优化

Linux 桌面用久了,迟早会碰到一个绕不开的话题:字体。刚装好系统那会儿,浏览器和终端里的中文看着还行,等你真正开始每天十几个小时盯着屏幕写代码、看文档、读日志,眼睛就开始提意见了——字重太细发虚、中文和英文不…

2026/9/17 3:58:59

2026年CPU散热器选购指南:从热力学原理到高性价比实战

1. 这不是“买哪个散热器”的问题,而是“你到底在给谁散热”的问题2026年谈CPU散热器,很多人第一反应是翻评测、看跑分、比价格——这就像看病只盯着药价,却没搞清自己得的是感冒还是心梗。我做硬件选型咨询和装机服务整整13年,经…

2026/9/17 3:58:59

汽车电子Simulink国产替代:MIL/SIL/HIL链路迁移与验证流程

上一篇聊"汽车行业为什么离不开 Simulink"发出去之后,后台收到的问题比我想的多得多,其中被问得最密集的一条是:那到底能不能换?国产替代这事儿,在汽车行业到底有没有可能性?说实话,这…

2026/9/17 3:58:59

索尼CD900与V600对比:第一代40mm监听单元的声音哲学与保养

最近把手头几台老索尼监听又翻出来挨个对比了一遍,最常听的就是MDR-CD900和MDR-V600。这两台机器放在一起特别有意思,都是第一代40mm动圈单元体系下的产物,但声音走向几乎是两个方向:CD900是冲着CD录音棚去的,清透、收…

2026/9/17 3:53:59

2026降AI率工具深度实测:从10款中筛选出3款真正能用的

先交代一下背景。2026年这个时间节点,AI生成内容在信息流、自媒体、电商文案、论文润色这些场景里的渗透率已经高得吓人,随之而来的就是各大平台和检测系统对“机器味”的识别越来越精准。我自己手里管着几个偏内容输出的账号,日常大量依赖AI…

2026/9/16 12:52:37

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/17 0:03:13

WiFi密码安全测试:从原理到实战的字典暴力破解指南

1. 写在前面:我为什么要研究WiFi密码这件事先交代一下背景。我身边有不少朋友,家里的WiFi密码常年是"12345678"或者"88888888",问就是"好记"。直到有一次,隔壁邻居蹭网蹭到我家路由器后台都进不去&…

2026/9/17 0:03:13

redis-py服务控制与监控函数实战:从ping到slowlog的巡检指南

我用 redis-py 写了快五年的业务代码,坦白说,真正让我觉得这个客户端“像一个成熟工具箱”的,不是 get/set 那套基本操作,而是它那批专门做服务控制与状态监控的辅助函数。日常开发里,大家把redis.Redis(host..., deco…

2026/9/17 0:03:13

SpringBoot+Vue3实现中小企业设备管理系统开发实践

1. 项目概述与核心价值中小企业设备管理系统是制造业、服务业等领域的基础信息化工具。传统设备管理往往依赖Excel表格或纸质记录,存在数据孤岛、流程混乱、维护成本高等痛点。这套基于Java SpringBootVue3MyBatis的技术方案,通过前后端分离架构实现了设…

2026/9/16 22:55:57

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/16 22:56:09

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/16 22:56:16

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码