尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
片元着色器入门:从零理解GPU逐像素着色原理与WebGL实战
这一篇我们聊片元着色器Fragment Shader。前面几篇把渲染管线和顶点着色器过了一遍之后很多零基础读者真正卡住的地方就出现在这里顶点着色器好歹还能和“坐标”“模型”联系起来片元着色器一上来就面对一堆颜色、坐标、插值完全不知道它在算些什么。但其实片元着色器是WebGL Shader里最容易出效果、也最能给你正反馈的部分。你不需要懂复杂的模型数据一个覆盖全屏的三角形就能画出来流动的渐变、翻涌的噪声、DEM高程设色甚至常见的流体模拟效果底层都有片元着色器的影子。这篇教程就围绕一个核心问题展开片元着色器到底是怎么决定屏幕上每一个像素的颜色的以及你该从哪里入手去改它、调它、玩它。这篇内容适合三类人刚看完顶点着色器还处于半懂状态的WebGL入门者用Three.js做可视化但想深入底层发现自定义材质无从下手的前端开发者以及用Unity、Cesium、MagicaVoxel这些工具时被“shader怎么这么玄乎”劝退想补上底层基础的同学。看完之后你会得到一个完整的最小可运行项目并且能自己改出渐变、圆形、动态闪烁这些基础效果。1. 片元着色器在渲染管线里的真实位置1.1 一句话搞懂片元着色器在做什么把现代GPU的渲染流程想象成一条流水线前端是建模中间是几何处理后端是光栅化之后的逐像素上色。顶点着色器负责处理模型每个顶点的位置、法线、纹理坐标它决定了“这个物体在屏幕上是什么形状”片元着色器则负责处理覆盖在屏幕上的每一个片元Fragment的颜色它决定了“这个形状的每一小块表面最终呈现出什么颜色”。片元这个词是GLSL规范里的正式叫法在中文语境里经常被简称为“像素着色器”。严格来说片元不等于像素一个像素在最终显示时可能由多个片元混合而成尤其在开启多重采样抗锯齿MSAA的时候一个像素会生成多个子样本片元分别执行片元着色器最后再合并成一个像素。但你在写Shader时可以先把它们当成同一个东西后面做抗锯齿和半透明混合时再仔细区分。我之前给零基础朋友打过一个比方顶点着色器是给车定型的钣金师傅负责把一块铁皮敲成车门片元着色器是喷漆师傅负责把车门刷成红色、带金属颗粒的那种红。钣金没做好车门的形状就歪了喷漆没喷好整个质感就会崩塌。在WebGL高手圈子里有一句话“顶点着色器决定形状片元着色器决定质感”这句话不夸张。1.2 顶点着色器和片元着色器如何分工这两种着色器最核心的区别在于它们执行次数和数据来源完全不一样。顶点着色器对每个顶点执行一次。一个三角形三个顶点不管这个三角形在屏幕上覆盖了多少像素顶点着色器都只跑三次。它只关心顶点的位置怎么变化、和模型相关的属性怎么传递。当顶点数量是几百个、几万个模型时顶点着色器的执行次数就是几万次这仍然是不大的数字。片元着色器则完全不同。一个三角形覆盖屏幕上的区域哪怕是全屏只画一个大三角形片元着色器也要对覆盖到的每个片元执行一次。以1080p为例1920乘1080大约是207万个像素也就是说即使场景里只有一个巨大三角形片元着色器也会运行约两百万次。如果你的场景里有复杂的几何体交叉、多个物体这个数字会呈几何级数增长。单次执行看起来都很快但架不住次数多。所以在优化WebGL性能时第一优先级永远是减少片元着色器的开销。比如片段里少用循环、少用if分支、不要在高精度浮点上做昂贵的运算这些都是因为这个着色器会被执行天文数字级别的次数。而不是因为GLSL语法限制你这么做。1.3 为什么画面质感由片元着色器决定你看到的三维画面里影响视觉感受的大多数元素都发生在片元阶段。光照一个表面受到光照后颜色如何随视线方向、光源方向、法线方向变化这些都是片元着色器里用漫反射模型、高光模型计算出来的。顶点着色器也可以算但计算结果只插值到三角形内部得到的往往是多边形感很强的“塑料脸”所以实时光照都会放到片元级别去逐片元计算体现更细腻的光影过渡。纹理采样给模型贴一张纹理图在片元着色器里根据uv坐标取出对应颜色这个过程叫纹理采样。uv经过GPU自动插值后每个片元都拿到了自己对应的纹理坐标因此能还原出照片级细节。描边、马赛克、霓虹渐变、噪点、流场扰动、波形扭曲这些后处理效果本质上都是把一个画面当作一张大纹理然后在片元着色器里对每个屏幕片元做各种数学变换。甚至一些貌似“几何”的效果也可以用片元着色器模拟。比如在ShaderToy上看到的那种看起来像是雕刻出的3D模型很多并不是真的三角形网格而是用光线步进Ray Marching在片元着色器里对每个屏幕片元发射射线去计算射线与物体表面的交点然后做光照和着色。这也是片元着色器被称作“每像素编程”的原因。2. 片元着色器的代码骨架与核心语法2.1 一个最简片元着色器逐行看懂先看一段在WebGL 1里最基础、最常用的片元着色器precision mediump float; void main() { gl_FragColor vec4(1.0, 0.5, 0.2, 1.0); }只有三行有效代码但每行都值得解释清楚。第一行precision mediump float;是精度声明。GLSL要求你在片元着色器里明确说明浮点运算的精度一般有三种highp、mediump、lowp。移动端GPU对highp的支持参差不齐所以在片元着色器里普遍用mediump。不写这一行很多设备的驱动会直接编译报错。第二行void main()是着色器入口函数和C语言很像。着色器程序不需要你手动调用它GPU在光栅化流程里会对每个片元自动执行一次main函数。第三行gl_FragColor是WebGL 1内置的输出变量。它是一个vec4也就是四个浮点数分别代表红、绿、蓝、透明度取值范围是0到1。注意不是0到255也不是0到100而是0到1。所以vec4(1.0, 0.5, 0.2, 1.0)表示的是不透明的橙色红色拉满、绿色一半、蓝色只有两成。WebGL 2和OpenGL ES 3.0之后推荐用自定义输出变量替代gl_FragColorprecision mediump float; out vec4 fragColor; void main() { fragColor vec4(1.0, 0.5, 0.2, 1.0); }在out vec4 fragColor;里out表示这是一个从着色器输出到下一个阶段的变量名字可以自己取。很多教程为了兼容性和教学简单还在用gl_FragColor所以我这一篇的示例会以WebGL 1为主你在看大多数在线Shader编辑器时也不会遇到困惑。2.2 gl_FragCoord 到底是坐标还是像素如果说gl_FragColor是片元着色器的“输出口”那gl_FragCoord就是片元着色器的“输入口”之一。它是GLSL内置变量类型是vec4每个片元执行时都会自动带上自己的窗口坐标信息。precision mediump float; void main() { // 红色分量用x坐标除以800绿色分量用y坐标除以600 float r gl_FragCoord.x / 800.0; float g gl_FragCoord.y / 600.0; gl_FragColor vec4(r, g, 0.0, 1.0); }这段代码里gl_FragCoord.x和gl_FragCoord.y就是当前片元在画布上的像素坐标左下角是起点(0,0)右上角是(canvas宽度, canvas高度)。我把x坐标除800得到0到1之间的数作为红色分量y坐标除600得到0到1之间的数作为绿色分量。这里有个新手必踩的坑gl_FragCoord的单位是物理像素不是CSS像素。如果你在CSS里把canvas放大到页面的100%宽度但canvas.width只设置了1280那gl_FragCoord的取值范围就只到1280超过1280的CSS像素区域会被拉伸或裁剪。所以做全屏效果时一定要保证canvas.width和canvas.height与实际物理像素一致。这个问题会在后面的分辨率章节详细说。另外gl_FragCoord.z和gl_FragCoord.w也和深度、透视除法相关但基础阶段用不到可以先忽略。2.3 varying插值为什么三角形内部颜色是渐变这是很多零基础读者最迷惑的点。我在顶点着色器里给了三个顶点三个不同的颜色为什么渲染出来的三角形内部颜色是一段平滑渐变而不是三个色块答案是两行代码之间隐藏的GPU自动插值。顶点着色器输出一个varying变量比如// 顶点着色器 attribute vec2 a_position; varying vec3 v_color; void main() { v_color vec3(1.0, 0.0, 0.0); // 假设这个顶点输出红色 gl_Position vec4(a_position, 0.0, 1.0); }三角形有三个顶点GPU分别执行三次这段代码于是三个顶点各自输出了一个v_color值。光栅化阶段GPU会把三角形覆盖到的每个片元都算一遍它在三角形内部的位置越靠近红色顶点拿到的v_color就越偏红越靠近蓝色顶点就越偏蓝。varying变量在顶点着色器中写入在片元着色器中读取中间由GPU在硬件层面完成线性插值。这个机制非常简单、非常快是整个渲染管线里最优雅的设计之一。生活里也可以找到例子你在一张白纸上涂三个点红色、绿色、蓝色然后用手指把它们抹开交界处一定是平滑的渐变色因为手指把三种颜色的颜料按距离混合了。GPU做插值本质上就是这个过程只是速度极快而且每个片元都精确地按重心坐标计算比例。所以下一章我们在做PBR或者材质效果时一定离不开varying因为你想让模型的每个位置都有自己独立的法线、uv、世界坐标而不是全屏统一数据。3. 从黑屏到渐变——最小WebGL项目实战3.1 搭建本地调试环境这个阶段的重点不是搭建复杂的三维引擎而是让你能最短路径地在浏览器里看到片元着色器运行结果。我推荐用本地静态服务器方式因为以后写文件上传、纹理加载时直接用file://协议打开会出现跨域问题。如果你装了Node.js可以在项目目录下运行npx serve .装过Python的话更省事python3 -m http.server 8080然后浏览器打开http://localhost:8080/把你的HTML文件地址写进地址栏。不想起本地服务器的话直接在CodePen或ShaderToy这类在线编辑器里写也可以。Shadertoy的代码模板会自动帮你把iResolution、iTime这些uniform传好非常适合做片元着色器练习。但为了不让你对“自动注入的uniform”产生依赖这篇还是用原生的方式把每一步都暴露在你面前。3.2 核心代码拆解编译、链接、绘制下面是一段完整的HTML代码它在全屏画布上绘制一个从左下角到右上角渐变的背景。你可以先直接跑起来然后我们再慢慢拆。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleWebGL 片元着色器入门/title style body { margin: 0; overflow: hidden; } canvas { display: block; width: 100vw; height: 100vh; } /style /head body canvas idcanvas/canvas script const canvas document.getElementById(canvas); const gl canvas.getContext(webgl) || canvas.getContext(experimental-webgl); if (!gl) { throw new Error(当前环境不支持 WebGL); } function resizeCanvas() { const dpr window.devicePixelRatio || 1; canvas.width Math.floor(window.innerWidth * dpr); canvas.height Math.floor(window.innerHeight * dpr); canvas.style.width window.innerWidth px; canvas.style.height window.innerHeight px; gl.viewport(0, 0, canvas.width, canvas.height); } resizeCanvas(); window.addEventListener(resize, resizeCanvas); const vsSource attribute vec2 a_position; void main() { gl_Position vec4(a_position, 0.0, 1.0); } ; const fsSource precision mediump float; uniform vec2 u_resolution; void main() { vec2 uv gl_FragCoord.xy / u_resolution; vec3 color vec3(uv.x, uv.y, 1.0 - uv.x); gl_FragColor vec4(color, 1.0); } ; function createShader(type, source) { const shader gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { throw new Error(Shader 编译失败: gl.getShaderInfoLog(shader)); } return shader; } const vs createShader(gl.VERTEX_SHADER, vsSource); const fs createShader(gl.FRAGMENT_SHADER, fsSource); const program gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { throw new Error(Program 链接失败: gl.getProgramInfoLog(program)); } gl.useProgram(program); const buffer gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1 ]), gl.STATIC_DRAW); const aPosition gl.getAttribLocation(program, a_position); gl.enableVertexAttribArray(aPosition); gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0); const uResolution gl.getUniformLocation(program, u_resolution); gl.uniform2f(uResolution, canvas.width, canvas.height); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLES, 0, 6); /script /body /html这段代码里最有必要拆开讲的有四个部分。其一resizeCanvas函数。它根据devicePixelRatio把canvas.width设为物理像素尺寸同时保持CSS里的显示尺寸为100vw和100vh。这样可以避免高分屏下画面模糊也不会让gl_FragCoord的坐标范围与CSS像素错位。我试过很多次这个函数本身就是个坑不处理dpr在Mac Retina屏上画面会发虚处理了dpr但忘记同步CSS尺寸画布会出现奇怪的裁切。其二顶点着色器里输入的a_position是-1到1的裁剪空间坐标。两个三角形拼成一个大四边形覆盖整个屏幕。坐标数据是-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1前三个顶点构成一个左下角的三角形后三个顶点构成右上角的三角形。这样整个四边形覆盖屏幕后片元着色器就对全屏每个片元运行一次。其三createShader函数里的编译错误处理。GLSL编译失败不会像JavaScript那样抛出异常而是静默地生成一个编译状态。你必须主动调用gl.getShaderParameter(shader, gl.COMPILE_STATUS)检查状态再用gl.getShaderInfoLog(shader)拿到日志。这段代码我每一次写WebGL项目都会保留它是排查片元着色器语法错误的第一道防线。其四gl.drawArrays(gl.TRIANGLES, 0, 6)。这里6是因为顶点数组里有6个顶点组成了两个三角形。用TRIANGLES绘制时GPU每3个顶点组成一个三角形6个顶点刚好组成两个。3.3 颜色渐变是如何算出来的上述代码在片元着色器里最重要的两行是vec2 uv gl_FragCoord.xy / u_resolution; vec3 color vec3(uv.x, uv.y, 1.0 - uv.x);gl_FragCoord.xy是当前片元的物理像素坐标比如(1200, 800)。u_resolution是画布物理尺寸比如(1920, 1080)。两者相除后得到uv取值范围是0到1表示当前片元在整个画布中的相对位置。左下角坐标gl_FragCoord接近(0,0)uv也接近(0,0)所以color是vec3(0.0, 0.0, 1.0)偏蓝色。右上角坐标uv接近(1,1)所以color是vec3(1.0, 1.0, 0.0)黄色。中间位置的颜色就是这三个分量按比例混合的结果。我建议你把颜色公式改成vec3(uv.x, 0.0, 0.0)你会看到一条从左到右的纯红色渐变改成vec3(0.0, uv.y, 0.0)会看到一条从下到上的绿色渐变。这种“随手改动颜色公式、立刻观察变化”的练习模式是整个入门阶段最有效的学习方法比看十篇教程都管用。4. 片元着色器核心逻辑的三个进阶练习4.1 坐标映射把屏幕变成0-1的uv坐标系刚接触Shader的人最容易在坐标问题上栽跟头。gl_FragCoord是像素坐标范围从0到画布宽高直接拿它做数学运算会出现一个问题同一个效果在不同分辨率的屏幕上显示不一致。解决办法就是上一节提到的归一化。把所有片元坐标都压到0到1之间得到uv坐标。这样你的Shader逻辑就和分辨率解耦了不管运行在1080p还是4K屏上画出来的图形形状、颜色分布都保持一致。precision mediump float; uniform vec2 u_resolution; void main() { vec2 uv gl_FragCoord.xy / u_resolution; float dist distance(uv, vec2(0.5, 0.5)); float circle smoothstep(0.15, 0.14, dist); vec3 color vec3(circle); gl_FragColor vec4(color, 1.0); }这里用distance函数算出当前片元和屏幕中心(0.5, 0.5)的距离再用smoothstep在半径0.15附近做一个平滑过渡。smoothstep(edge0, edge1, x)的意思是当x小于edge0时返回0大于edge1时返回1中间做平滑插值。写法上smoothstep(0.15, 0.14, dist)看起来很反直觉但实际上是利用平滑过渡的边缘来产生抗锯齿圆形边缘宽度就是0.15减0.14之间的范围。这里补充一个细节如果你用if (dist 0.15)这种写法也能画出圆形但边缘会非常粗糙产生锯齿。GPU光栅化产生的边缘锯齿只能通过这种平滑的数学函数消除这也是为什么Shader编程和普通编程思维不一样——它鼓励你用数学函数代替分支用连续变化代替离散判断。4.2 时间驱动用uniform把动画“喂”给GPUShader写好了怎么让它动起来答案是传入一个时间uniform让每一帧传入的数值都不同。JavaScript侧你需要用requestAnimationFrame不停地更新这个值let startTime performance.now(); function render(now) { const time (now - startTime) / 1000.0; // 秒 gl.uniform1f(timeUniformLocation, time); gl.drawArrays(gl.TRIANGLES, 0, 6); requestAnimationFrame(render); } requestAnimationFrame(render);GLSL侧片元着色器里这样写precision mediump float; uniform vec2 u_resolution; uniform float u_time; void main() { vec2 uv gl_FragCoord.xy / u_resolution; float r 0.5 0.5 * sin(u_time uv.x * 6.28318); float g 0.5 0.5 * sin(u_time * 0.8 uv.y * 6.28318); float b 0.5 0.5 * sin(u_time * 1.2 length(uv) * 6.28318); vec3 color vec3(r, g, b); gl_FragColor vec4(color, 1.0); }sin函数的取值范围是-1到1通过0.5 0.5 * sin(x)映射到0到1正好对应颜色分量。关键点在于给不同分量设置不同的频率和相位u_time * 0.8和u_time * 1.2让三个颜色通道的变化速度不一致uv.x * 6.28318和uv.y * 6.28318则让颜色在空间方向也产生波动。6.28318是2π的近似值。乘以它意味着每经过一个周期sin内部的角度变化正好是一个完整圆周这样画面会出现连续、平滑的循环波动不会突然跳变。这里有个经验性问题很多初学者喜欢把时间乘以一个很大数字追求“快速变化”结果画面闪成电焊现场。我建议初始阶段把时间速度因子控制在0.5到2.0之间优先观察颜色流动的规律再通过调参去逼近你想要的效果。4.3 varying传值给将来的模型纹理打好基础全屏Shader的清晰之处在于不需要考虑顶点属性直接把gl_FragCoord当输入。但你做的项目不可能一直都是全屏后处理总会遇到给一个3D模型贴纹理、涂颜色的场景。这时候要让片元着色器拿到每个片元对应的纹理坐标就必须从顶点着色器传值过来。顶点着色器attribute vec2 a_position; varying vec2 vUv; void main() { vUv a_position * 0.5 0.5; gl_Position vec4(a_position, 0.0, 1.0); }a_position的范围是-1到1做a_position * 0.5 0.5后范围变成0到1。这就是把裁剪空间坐标映射成纹理坐标uv的常见写法。片元着色器precision mediump float; varying vec2 vUv; void main() { vec3 color vec3(vUv.x, vUv.y, 1.0); gl_FragColor vec4(color, 1.0); }这里不再用gl_FragCoord而是直接用从顶点着色器插值而来的vUv。渲染出来的效果和用gl_FragCoord / u_resolution的效果几乎一样但原理完全不同。一个是片元自己算出来的屏幕坐标一个是顶点着色器传值后由GPU插值得到的坐标。后者才是模型纹理采样的标准做法。你还可以基于vUv画网格线precision mediump float; varying vec2 vUv; void main() { vec2 grid abs(fract(vUv * 10.0) - 0.5); float line 1.0 - step(0.02, min(grid.x, grid.y)); vec3 color mix(vec3(0.1), vec3(1.0), line); gl_FragColor vec4(color, 1.0); }fract取小数部分vUv * 10.0把屏幕分成10乘10的网格abs(x - 0.5)保证网格线在格子两侧是对称的step决定哪些位置的亮度高。这段代码体现了一个思维模式用数学函数组合出几何图案而不是用if分支逐格判断。5. 常见问题与性能避坑实录5.1 黑屏但不报错这是新手阶段最令人抓狂的现象网页打开控制台干干净净canvas区域一片漆黑。我总结了一下原因按出现频率排序如下。第一片元着色器没有给所有路径写入颜色。比如你在某个if分支里写了gl_FragColor但另一个分支没写或者某些提前return的路径漏掉了赋值最终渲染时GPU可能拿到一个未定义的颜色显示为黑色。第二顶点数据范围不对。很多人习惯写0到1的坐标但裁剪空间是-1到1如果顶点都集中在屏幕左下角看到的画面就接近全黑只有角落有颜色。检查方法很简单把顶点数据改成覆盖全屏的四个点或六个点。第三program没有生效。正确顺序是先gl.useProgram(program)再drawArrays。漏掉useProgram时GPU使用的默认着色器程序什么都不画。第四gl.viewport没有同步画布尺寸。全屏Shader因为uv是相对坐标可能看起来好像没太大影响但如果你同时设置gl_Position和viewport不一致画面会被裁剪错乱。排查顺序我推荐这样先在片元着色器里写死颜色比如gl_FragColor vec4(1.0, 0.0, 0.0, 1.0)如果画面变红说明数据流没问题问题出在颜色公式或uniform传参上如果还是黑屏检查顶点数据、program链接状态和viewport。5.2 GLSL语法容易踩的三个坑GLSL和JavaScript虽然都长得像C语言但细节上坑很多。隐式类型转换不存在。float value 1;在JavaScript里完全正常但在GLSL里直接编译失败必须写成1.0。同理vec2(1, 2)也会报错因为整型字面量不能直接当浮点数用。我一开始写Shader时几乎每周都会因为这种小问题浪费时间后来索性写任何数字都带上小数点。精度声明缺失。片元着色器里如果不写precision mediump float;在很多设备的驱动上会报错。如果你用的设备是高通Adreno或者老一点的Mali GPU这一类编译错误尤其常见。规范允许不声明但实际终端设备对“默认精度”的实现并不统一所以写的时候一定带上。矩阵和向量的乘法顺序容易混淆。GLSL里vector * matrix和matrix * vector语义不同前者是按列向量处理后者按行向量处理。WebGL的习惯是使用列主序矩阵代码里一般是vector * matrix。你暂时用不到矩阵运算但后续做模型变换时会遇到这个坑先记住结论。5.3 分辨率与DPR的坑全屏Shader最常见的模糊问题罪魁祸首通常是canvas物理尺寸和CSS尺寸不一致。从CSS角度来看你设置了canvas { width: 100vw; height: 100vh; }浏览器会把canvas显示尺寸拉伸到窗口大小。但canvas.width和canvas.height属性决定的是画布里的像素数量。如果两者不一致WebGL渲染到canvas.width那个分辨率再由CSS拉伸到显示尺寸就会出现模糊或锯齿。正确做法我在这篇的代码里已经写进去了用window.devicePixelRatio乘上窗口尺寸再赋给canvas.width和canvas.height同时把CCS尺寸设置为窗口尺寸。另外gl_FragCoord用的是物理像素坐标不是CSS像素。如果你在片元着色器里把某一段距离硬编码为比如50.0在普通屏上是50像素在Retina屏上看起来只有25像素的视觉宽度。所以做尺寸相关的效果时最好也把devicePixelRatio作为uniform传入着色器或者直接用归一化后的uv计算。5.4 性能建议速查表情况建议片元着色器里有循环尽量少用循环次数固定的尽量展开写。GPU对动态循环支持差最好避免片元着色器里有if/else可用step、smoothstep、mix等数学函数替代分支纹理采样次数太多每多一次纹理采样都消耗带宽能预计算的先传给uniform使用highp精度片元着色器里非必要不使用mediump够用且更快大数组、动态索引避免在片元着色器里读取动态索引的大数组开销极大依赖gl_FragCoord做全屏后处理如果用不上像素坐标优先用varying插值传递uv省去uniform传递这套表是给后续做真实项目准备的。入门学习阶段不必过度优化一个渐变背景就算写一百个分支也照样跑得飞起。但当你开始做粒子、流体模拟、复杂后处理时片元着色器会成为整个渲染流程的瓶颈这些性能原则就要提前内化成肌肉记忆。另外提一个非常容易忽略的坑片元着色器的变量精度不只是声明而已它还会影响纹理坐标的插值精度。使用mediump时uv在某些大尺寸贴图上可能出现轻微偏移或细纹现象如果你观察到纹理在远处闪烁、抖动可以先检查精度必要时单独把uv声明为highp。最后再分享一点个人体会片元着色器是我个人觉得WebGL整个体系里最“好玩”的部分。它把编程从“逻辑堆叠”变成了“数学作画”同样的一个圆形放大坐标可以画出星球环加噪点可以画出火焰纹理加时间可以做呼吸光效。我一开始学的时候就是每天打开一个空白的Shader模板随意改几个数字观察画面的变化这种即时反馈带来的快乐比写完一整页业务代码强太多了。对刚看完这篇的读者我的建议只有一个把你手头那个渐变动画的代码打开随便改。改成红色分量为uv.x * uv.y改成绿色分量为1.0 - uv.y或者给三个通道加上不同的时间速度再看效果。改到你觉得“这个如果用于某个项目背景会很好看”为止你的片元着色器入门就算真正完成了。下一篇我会接着讲GLSL内置函数和更丰富的颜色数学玩法。
RELATED

相关推荐

OpenShell实战:跨平台终端会话管理与效率增强工具全解析

OpenShell实战:跨平台终端会话管理与效率增强工具全解析

1. 项目概述与核心价值 1.1 OpenShell 到底是什么 第一次听到"OpenShell"这个名字,很多人会下意识以为是某个操作系统的开源替代品,或者是某种远程连接工具。其实都不完全是。OpenShell 是一个面向命令行重度用户的 跨平台终端效率增强工具集…

📅 2026/10/3 18:52:15
昇思MindSpore中max_lr调优实战:从NaN到收敛

昇思MindSpore中max_lr调优实战:从NaN到收敛

前几天帮一位师弟调试CIFAR-10图像分类的小网络,他跑了一个晚上,loss曲线像坐了过山车:前面几个epoch还在正常下降,到第五个epoch左右直接飙成NaN。我盯着训练日志看了很久,排除掉数据、归一化、模型结构一堆嫌疑之后&…

📅 2026/10/3 18:52:15
福建DEM TIFF数据处理指南:坐标系、高程基准与精度验证

福建DEM TIFF数据处理指南:坐标系、高程基准与精度验证

简介:本资源为福建省全域高精度数字高程模型(DEM)原始数据集,面向地理信息、遥感、城乡规划、防灾减灾等领域的科研人员、GIS工程师及高校师生,用于地形分析、水文模拟、坡度坡向计算、三维可视化等核心空间建模任务。…

📅 2026/10/3 18:47:15
MORE NEWS

更多资讯

📰

告别本地环境!20款在线ESP开发工具与Web Serial烧录实战

1. 为什么我彻底放弃了本地搭建 ESP 开发环境 三年前我第一次接触 ESP32 的时候,光是装开发环境就折腾了整整两天。Arduino IDE 下载卡在 30% 不动,换了国内源之后又遇到版本不匹配,好不容易装完了,编译一个最简单的点灯程序报了一…

📰

AI写嵌入式驱动:如何避免刷砖并高效辅助开发

1. 为什么“AI写驱动”这件事在嵌入式圈子里争议这么大先把结论摆在最前面:AI 可以帮你写驱动,但绝对不能替你决定驱动该怎么写。这两句话听起来像绕口令,但差别大了去了。前者是“你主导、AI 辅助”,后者是“AI 主导、你背锅”&a…

📰

AI 智能体落地难的真实原因:从 RAG 到 LLM 工程化,TaoToken 统一 Key 能解决什么?

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

📰

最新DeepSeek-V3驱动的MCP与SemanticKernel实战教程:TaoToken统一Key接入智能应用全流程

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

📰

图解AI 11种Agent生态全景:从RAG到编程实战,TaoToken统一Key接入指南

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

📰

阿里云百炼 MCP 部署实战:把本地代理失败改到 TaoToken 的排查路径

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

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬