尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GLSL内置函数深度解析:从smoothstep到fwidth的实战指南

GLSL内置函数深度解析:从smoothstep到fwidth的实战指南 前阵子调一个边缘光效的着色器问题出在smoothstep的边界条件上我图省事把两个edge参数传成了同一个值结果在NVIDIA显卡上正常在另一台AMD机器上直接出现整片闪烁。查来查去最后老老实实打开《OpenGL Shading Language Specification》里的Built-In Functions章节才发现规范里清清楚楚写着如果edge0 edge1结果是未定义的。这件事让我下了决心把手头零散的GLSL内置函数知识重新按规范捋一遍。这篇稿子就是那次梳理的产物。OpenGL Shading Language的Specification分很多章但内置函数这一章是最值钱的——它是着色器的标准库也是很多人最容易一知半解的地方。今天我不打算像字典一样罗列函数签名而是想讲清楚这些内置函数为什么这么设计、实际项目里怎么用、遇到渲染结果不对时怎么排查。内容偏实战适合刚把GLSL语法过了一遍、想系统提升的读者也适合那些被shader编译错误和平台差异折腾过的人。1. 内置函数在规范文档里的定位与阅读方法1.1 为什么官方文档值得硬啃一遍现在网上的GLSL教程非常多但大部分是函数功能速查级别的搬运很多文章连GLSL版本都不标注。同一个函数在GLSL 1.20和GLSL 4.60里的行为可能差很远。比如早期教程里常见的texture2D在GLSL 1.30之后逐渐被统一的texture入口取代再比如内置的noise1到noise4函数虽然规范一直保留但实际驱动厂商很少高质量实现几乎属于存在但不可依赖的状态。这些坑只有回到规范文档本身才能看清。《OpenGL Shading Language Specification》通常和OpenGL规范一起放在Khronos官网里的内置函数部分按照功能把函数分成角度与三角函数、指数函数、通用函数、几何函数、矩阵函数、向量关系函数、纹理函数、偏导数函数、插值函数、整数函数、原子函数、打包解包函数等大类。每类函数给出了完整的重载签名、定义域、返回值约定以及在某些参数条件下结果是未定义的这类关键警告。如果你工作中经常写shader花一个下午把这一章通读一遍效果会比收藏几十篇教程好得多。我个人的建议是先看目录把函数分类地图印在脑子里再挑自己项目里常用的几类细读比如纹理采样和几何函数最后把不认识的函数名过一遍知道有这个东西将来可能用得上即可。不需要背签名但一定要知道哪些函数存在、哪些函数有坑。1.2 函数重载与genType约定GLSL的泛型机制初次翻开规范可能会被满屏的genType、genIType、genUType、genDType、genBType搞懵。这其实是GLSL表达函数重载的方式意思是同一个函数名可以接受标量、向量、矩阵等多种类型组合。genType代表float、vec2、vec3、vec4中的任意一种genIType对应int和ivec*genUType对应uint和uvec*genDType对应double和dvec*。所以当规范写genType abs(genType x)时意思是abs(float)、abs(vec2)、abs(vec3)、abs(vec4)四个重载全都有。genBType abs(genBType x)这种则代表bool以及bvec*也参与重载比如mix函数就有一个使用bvec作为选择器的版本。理解这个机制很重要的一点是GLSL没有模板它靠这种约定符号表达多个重载。写代码时不需要像C那样显式指定类型只要传参类型匹配某个重载即可。但这也意味着传错类型时编译器可能给出非常含糊的no matching overloaded function错误。遇到这种错误先不要怀疑人生大概率就是参数类型不匹配比如给mix传了三个vec3但选择器传成了float或者给texture传了整数坐标而不是浮点坐标。1.3 内置变量和内置常量内置函数的前置条件内置函数不是凭空起作用的。很多函数直接依赖OpenGL管线写入的内置变量比如顶点着色器必须写gl_Position片元着色器可以读gl_FragCoord、gl_FrontFacing几何着色器里有gl_PrimitiveIDIn计算着色器里有gl_GlobalInvocationID。这些内置变量构成了shader和固定管线之间的接口理解它们才能理解函数的输入输出。同样重要的是内置常量比如gl_MaxVertexAttribs、gl_MaxTextureImageUnits、gl_MaxCombinedTextureImageUnits、gl_MaxDrawBuffers等。这些常量在shader里直接可用用来做兼容性保护和资源上限判断。比如你想在一个片段着色器里绑8张纹理但目标设备只支持gl_MaxTextureImageUnits 16理论上没问题但如果超过上限编译就会失败。规范里还有大量类似的上限常量分布在各个渲染阶段。我的习惯是在跨平台渲染器里写一个公共的limits.glsl头文件把这些常量统一引用一遍运行时再配合glGetIntegerv核对能提前暴露很多驱动差异。2. 常用内置函数比怎么用更重要的是为什么2.1 角度、三角与指数函数GPU数字计算的底层偏好radians和degrees是很多新手第一个接触的内置函数。GLSL里的三角函数默认采用弧度制但引擎策划和美术资源经常使用角度所以radians(45.0)就成了常规写法。为什么GPU不直接支持角度制原因是弧度制在数学上更自然泰勒展开、微分、向量旋转矩阵全部默认弧度硬件实现也倾向于直接计算弧度输入。这件事没有太多高深道理但记住一点shader里大量使用角度时需要小心浮点精度尤其当角度值很大时比如累积旋转角度超过了数万度在mediump精度下三角函数的误差会非常明显。我遇到过粒子系统旋转角度累加导致最终位置漂移的bug最后把旋转角度先mod到[0, 360)再用radians转换问题就消失了。pow、exp、exp2、log、log2、sqrt、inversesqrt这一族函数设计上有很深的硬件考量。GPU原生计算指数时exp2和log2往往是最直接的指令pow(x, y)在底层很可能就是exp2(y * log2(x))。因此如果底数是2的幂次直接写exp2(k)会比pow(2.0, k)更高效在循环量大的shader里能省下不少指令。inversesqrt也非常值得注意normalize、distance这类函数内部通常会调用它因为GPU实现1.0 / sqrt(x)时用一条rsq级别的近似指令往往比先开方再除法更快。我们自己写归一化代码时也可以刻意使用inversesqrt而不是1.0 / length(...)。2.2 向量与矩阵运算函数光照计算背后的数学闭环几何函数是3D光照shader的基石。dot(N, L)计算余弦值cross(A, B)构建正交轴normalize把向量归一化reflect(I, N)实现镜面反射refract(I, N, eta)实现折射faceforward(N, I, Nref)根据入射方向和参考法线翻转法线方向。这些函数单独看都是简单的数学公式但组合起来就能完成Blinn-Phong、PBR、视差映射、菲涅尔等一系列效果。这里特别想说说faceforward。很多人在手写法线翻转时喜欢用if (dot(N, I) 0.0) N -N;这种分支但faceforward在大多数GPU上可以映射为一条条件选择指令更简洁也更不容易出错。还有reflect它要求入射向量I和法线N都是归一向量的前提在数学上是隐含的实际使用时如果法线没归一化反射方向会直接偏掉。我见过一个SSR效果里反射光线歪斜排查半天发现是法线贴图采样后忘了重新归一化而这种bug在静态画面里极难察觉一旦相机运动就彻底露馅。矩阵函数里transpose、determinant、inverse属于存在但慎用的类别。inverse的开销明显大于其他矩阵函数在片段着色器里对每个像素求逆矩阵属于性能杀手。我通常会把逆矩阵放到顶点着色器或CPU端计算好再通过uniform传入片元阶段。matrixCompMult也经常被误解它不是矩阵乘法而是把两个矩阵对应分量相乘真正的矩阵乘法用*运算符并且要遵守左矩阵列数等于右矩阵行数的规则。2.3 量化、步进与平滑过渡程序化纹理的常用积木floor、ceil、fract、mod、modf、min、max、clamp、mix、step、smoothstep这组函数看起来是简单的数学工具实际上撑起了程序化纹理和UI渲染的大半壁江山。fract配合floor可以实现无限平铺网格、地板砖纹理、分形坐标折叠。比如做水流波纹时vec2 uv fract(worldPos.xz * 4.0);就能让纹理在平面上重复四次。mod在GLSL里有自己的符号规则结果与除数y同号这个细节和数学里的取模不太一样写循环坐标时要特别注意。modf(x, out intPart)可以同时取出整数部分和小数部分适合做渐变分段的颜色量化。mix(a, b, t)是线性插值函数但它还有一个冷门重载mix(a, b, boolSelect)或mix(a, b, bvec selectors)。当选择器是布尔向量时它会逐分量从a或b里取值相当于一个无分支的条件选择。这在shader里非常有用——既能绕过if带来的分支开销又不会像step那样产生硬边。比如做昼夜循环时用mix(dayColor, nightColor, greaterThan(worldY, 0.0))就能按地形高度无分支地混合两种颜色。smoothstep是GLSL里我最偏爱的函数之一。它的原理是三阶Hermite插值从edge0到edge1之间函数值从0平滑过渡到1并且在两端的一阶导数为0所以视觉上没有突兀的转折点。实际项目中smoothstep几乎就是边缘抗锯齿的代名词。UI圆角矩形、圆形按钮、SDF字体、水波纹边缘都能看到它的身影。注意规范明确规定如果edge0 edge1结果是未定义的所以传入参数前一定要保证edge顺序正确。我踩过的那个坑就是两个edge相同导致有平台直接闪烁。3. 纹理采样函数群从texture到textureGather的完全指南3.1 texture函数族的参数体系旧时代与新时代的入口差异纹理采样是片元着色器最重要的功能之一也是内置函数数量最多、最容易用错的地方。老式GLSL里的texture2D、textureCube、texture3D在规范演进中被统一归并到texture函数名下现代代码里应该尽量使用统一入口。texture根据第一个参数是sampler2D、sampler3D、samplerCube还是sampler2DArray会自动选择对应重载。基本采样调用是vec4 color texture(sampler, uv)返回经纹理过滤和mipmap选择后的颜色。如果纹理绑定了mipmapGPU会通过屏幕空间导数自动计算应该采样哪一层LOD这个自动过程对开发者是透明的。但有些场景需要手动控制texture(sampler, uv, bias)可以在自动LOD基础上叠加一个偏置最常见的用途是让纹理稍微模糊或锐化一点但规范明确规定bias参数只能在片元着色器里使用。顶点着色器里没有屏幕空间导数所以需要用textureLod(sampler, uv, lod)显式指定采样层级。这在大地形、海洋、天空盒的顶点着色器采样中非常常见。textureProj是另一个高频函数它把坐标当成齐次坐标自动做除法比如用vec3(x, y, z)采样2D纹理时内部会取x/z, y/z作为实际纹理坐标非常适合阴影贴图里的投影纹理采样。3.2 偏导数与自动细节等级fwidth为什么是所有抗锯齿方案的基础dFdx、dFdy、fwidth、dFdxFine、dFdyFine、dFdxCoarse、dFdyCoarse这组函数是GPU渲染模型里最独特的工具。GPU执行片元着色器时通常以2x2像素块为单位并行处理所以任何一个变量在相邻像素之间都能算出这行代码在这个像素里的变化率也就是偏导数。fwidth(x)的定义是abs(dFdx(x)) abs(dFdy(x))代表该变量在屏幕空间的变化幅度。这个特性的杀手级应用是抗锯齿。渲染SDF字体时距离场数值在字体边缘从正到负快速变化直接拿它做透明度会产生严重的锯齿。经典解法是float dist texture(sdfTexture, uv).r; float aa fwidth(dist); float alpha smoothstep(0.5 - aa, 0.5 aa, dist);用fwidth作为边缘过渡宽度字体在任何DPI、任何缩放倍数下都能保持约一像素的平滑边缘效果比固定aa0.05的方案好得多。我说这句话是有代价的——以前固定值方案在高分屏上看还行一到低分辨率就糊成一片换了fwidth之后才算彻底解决。用偏导数函数时有个大坑GPU只在相邻像素共享同一控制流时才能正确计算导数。如果在if分支里调用dFdxGPU通常会先把两个分支都执行一遍导致性能显著下降甚至在某些硬件上产出未定义结果。非均匀控制流不同像素走不同分支越复杂偏导数越不可靠。所以不要在大范围条件分支里依赖fwidth尽量把导数计算放在分支外面。3.3 textureGather与texelFetch绕过自动过滤的进阶姿势textureGather是我前两年才开始频繁使用的函数因为它和常规采样逻辑差别很大。它不返回过滤后的颜色而是返回采样点周围2x2纹素块中某一个通道的四个值打包在vec4里返回。第一次用的人很容易把它当成返回4x4纹素或者返回vec4(rgba)这是最常见的误解。textureGather的典型场景是做自定义过滤。默认的纹理双线性过滤是硬件固定的如果想实现可编程的过滤半径更高质量的锐化或边缘感知的插值就可以先用textureGather把相邻纹素取出来在shader里自己算权重。配合textureSize获取纹理尺寸还能做出分辨率无关的采样逻辑。texelFetch则走了另一个极端它完全绕过纹理过滤、mipmap和坐标归一化直接用整数纹素坐标取原始数据。这让它非常适合在计算着色器里做物理精确的像素操作比如图像处理、体素读取、模拟数据采样。注意坐标边界问题——越界访问会得到未定义行为许多驱动只返回边界纹素或黑色调试起来很迷惑。我在写高斯模糊的compute shader时就会用texelFetch读取图像配合ivec2坐标循环逻辑比texture清晰得多。4. 从规范到落地内置函数实战的五个典型场景4.1 场景一SDF字体渲染中的fwidth组合拳SDF有向距离场字体是现代UI引擎里非常流行的方案。它的核心思路是把字体的几何形状预计算成一张距离场纹理每个纹素存储离最近边缘的距离然后shader里用距离值做硬阈值或软阈值渲染。实际渲染时我通常先把距离值从纹理里取出来再用fwidth动态计算抗锯齿宽度float dist texture(sdfTex, uv).r; float aa fwidth(dist); float alpha smoothstep(0.5 - aa, 0.5 aa, dist); color vec4(textColor.rgb, textColor.a * alpha);这段代码组合了纹理采样、偏导数、平滑过渡三类内置函数效果非常稳定。这个场景让我意识到内置函数不是孤立存在的真正的威力来自组合。比如再做描边效果只需要在dist上叠加一个abs(dist)运算就能让字体边缘内外两侧都出现边界。配合缩放时距离场天然支持字体放大而不失真前提是fwidth计算正确。4.2 场景二程序化地形与自定义噪声函数规范文档里的noise1到noise4虽然被列为内置函数但在实际项目中几乎没人直接用。原因很简单规范没有规定噪声的具体算法不同驱动可能返回完全不同的图案而且质量普遍一般。所以做程序化地形、云层、火焰这类效果时大家都是自己写hash噪声。GLSL里有一个非常流行的整数hash函数正好用到内置整数运算函数uint hash(uint x) { x ^ x 16; x * 0x7feb352dU; x ^ x 15; x * 0x846ca68bU; x ^ x 16; return x; } float rand01(uint seed) { return float(hash(seed)) * (1.0 / 4294967295.0); }然后对二维坐标做分层噪声float valueNoise(vec2 p) { ivec2 pi ivec2(floor(p)); vec2 pf fract(p); vec2 u pf * pf * (3.0 - 2.0 * pf); // smoothstep的原始形态 float a rand01(uint(pi.x)); float b rand01(uint(pi.x 1)); float c rand01(uint(pi.y)); // 实际上要4个随机值做双线性插值这里略写 }这个场景大量用到了floor、fract、mix、smoothstep的数学实现也体现了一个核心观点内置函数是积木但积木怎么拼取决于你对图形学算法的理解。4.3 场景三计算着色器里的原子操作与共享内存现代OpenGL里计算着色器Compute Shader越来越多地承担GPU通用计算任务。规范里原子操作函数主要集中在atomicAdd、atomicMin、atomicMax、atomicAnd、atomicOr、atomicXor、atomicExchange、atomicCompSwap。它们的线程安全由GPU硬件保证适用于直方图统计、归并排序、全局计数器等场景。直方图统计是典型的原子操作应用shared uint histogram[256]; // 初始化 if (gl_LocalInvocationIndex 0u) { for (int i 0; i 256; i) histogram[i] 0u; } barrier(); uint bin uint(clamp(color.r * 255.0, 0.0, 255.0)); atomicAdd(histogram[bin], 1u); barrier();注意barrier()必须放在写共享内存后、读共享内存前否则会出现数据竞争。原子操作本身有性能成本如果多个线程同时写同一个地址硬件需要串行化访问。我在做粒子统计时会把原子操作分散到多个局部通道里末尾再合并能明显降低热点竞争。4.4 场景四数据打包解包与顶点数据压缩GLSL 4.x之后提供了一组非常有用的数据打包函数packUnorm2x16、packSnorm2x16、packHalf2x16、packUnorm4x8、packSnorm4x8以及对应的unpack*版本。它们能在浮点数和整数位模式之间快速转换非常适合压缩带宽。比如粒子系统里我们经常需要把UV、法线方向、颜色之类的属性存到SSBO或VBO里。如果直接存两个float要8字节如果用packUnorm2x16打包成uint只要4字节。写入时uint packedUV packUnorm2x16(vec2(uv.x, uv.y));读取时vec2 uv unpackUnorm2x16(packedUV);这种压缩在移动端尤其重要因为GPU带宽通常比桌面端紧张得多。不过要注意精度unorm16只有16位精度适合0到1范围内的数据如果是大范围坐标还是要用half或float。4.5 场景五插值限定符与interpolateAt系列函数flat、smooth、noperspective是顶点着色器向片元着色器传递数据时的三种插值方式。smooth是默认的透视校正插值适合纹理坐标、法线、世界坐标flat不做插值适合图元ID、材质索引noperspective在屏幕空间做线性插值适合一些特殊效果和调试显示。interpolateAtCentroid、interpolateAtSample、interpolateAtOffset这三个内置函数允许你在片元着色器里手动控制插值位置而不是使用GPU自动选择的像素中心。interpolateAtSample在MSAA渲染中特别有用——它可以让你在某个具体采样点处重新计算插值结果从而得到更准确的边缘颜色。我做过一次MSAA下的边缘锐化通过遍历gl_NumSamples并用interpolateAtSample逐采样点取值边缘质量提升非常明显。注意这类函数不能用于flat声明的变量而且offset必须落在像素簇有效范围内。5. 环境配置与调试技巧从llvmpipe到老项目的GLSL问题5.1 llvmpipe软渲染没有独显也能调GLSL很多开发者在远程服务器、虚拟机或者没装独立显卡的机器上跑OpenGL程序一查渲染器字符串会看到类似GL_RENDERER llvmpipe (LLVM 15.0.7, 256 bits)。这不是环境坏了而是Mesa的软件光栅化后端在代替GPU工作。llvmpipe最大的价值是能提供一个相对完整的OpenGL实现——通常支持OpenGL 4.5甚至4.6核心配置这意味着你可以在没有GPU的CI环境里做GLSL语法编译、内置函数调用、基础渲染逻辑的自动化测试。但同时也要清楚它的限制CPU模拟渲染的速度非常慢纹理过滤、mipmap、原子操作、SSBO读写等硬件的加速特性完全不存在。在llvmpipe上测试通过不代表在真机上性能没问题反之真机上的某些驱动bug在软渲染环境里也完全复现不出来。我建议的做法是在CI脚本里设置LIBGL_ALWAYS_SOFTWARE1强制使用软渲染跑一遍基础的shader编译和输出比对只做正确性验证性能测试必须放到真实GPU环境。如果你在桌面上发现自己某一天渲染特别卡先跑一句glGetString(GL_RENDERER)看到llvmpipe就说明当前会话没启用硬件加速这能帮你快速判断是驱动问题还是API使用问题。5.2 VS2010这种老工具链里怎么配GLSL环境Windows平台的OpenGL有一个历史问题系统自带的gl.h只支持到OpenGL 1.1GLSL时代的所有函数比如glCreateShader、glCompileShader、glUseProgram都需要通过扩展机制加载。解决这个问题最常用的库是GLEW或GLAD它们能在运行时把函数指针取出来。VS2010是很老的工具链但项目里真的还有人用。配置步骤大致是下载GLEW 1.13.0或更新的版本把include目录和lib目录填到项目属性里代码里#include GL/glew.h并链接glew32.lib。初始化时glewExperimental GL_TRUE; GLenum err glewInit(); if (err ! GLEW_OK) { fprintf(stderr, GLEW error: %s\n, glewGetErrorString(err)); return -1; }注意glewInitialize调用必须在创建OpenGL上下文之后。用VS2010时还有一个坑它的C标准支持比较弱如果用了更新的第三方头文件可能编译不过。所以老项目里尽量用与编译环境匹配的老版本库没有必要追新。shader编译失败时一定要打印信息日志这是最直接的排查手段void dumpShaderLog(GLuint shader) { GLint len 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, len); if (len 1) { std::vectorchar log(len); glGetShaderInfoLog(shader, len, NULL, log[0]); fprintf(stderr, %s\n, log[0]); } }5.3 内置函数与GLSL版本选择版本决定函数可用性OpenGL版本和GLSL版本是一一对应的关系。OpenGL 2.0对应GLSL 1.102.1对应1.203.0对应1.303.1对应1.403.2对应1.503.3对应3.304.0对应4.004.5对应4.504.6对应4.60。内置函数在不同版本里的集合不一样比如textureGather在GLSL 4.0引入atomicAdd在计算着色器支持之后引入interpolateAtSample在GLSL 4.0引入。跨平台项目尽量把#version定在一个合理的下限。我会优先选择#version 330 core因为OpenGL 3.3在桌面平台上普及率很高而现代内置函数已经基本可用了。如果必须兼容移动端就要考虑GLSL ES版本它在精度限定符和部分函数行为上有所区别。运行时可以用glGetString(GL_SHADING_LANGUAGE_VERSION)确认驱动支持的GLSL版本再根据版本决定代码路径。这个检查看起来不起眼但能避免在老旧驱动上加载新版shader导致的编译失败。6. 常见问题与排查技巧实录6.1 编译错误函数重载找不到先查版本和签名GLSL编译报no matching overloaded function是高频问题。通常有三个原因函数名在当前的GLSL版本里不存在比如在GLSL 1.30之后用texture2D参数类型和重载不匹配比如给texture传整数坐标函数本身被限制在特定阶段比如dFdx只能在片元着色器使用。排查路径很固定先看#version再查规范函数签名最后确认当前阶段是否支持。6.2 精度问题同一个shader在不同硬件上效果差很多桌面GLSL默认使用highp但移动端GLSL ES经常需要显式指定精度。在mediump精度下sin、cos、atan这类函数的角度误差会放大粒子系统坐标累积后漂移明显。我的经验是涉及世界坐标、法线、矩阵运算的变量一定要用highp纯颜色、UV这类0~1范围内且对精度不敏感的可以用mediump。如果发现高光闪烁、边缘抖动、粒子位置漂移优先怀疑精度。6.3 软渲染下纹理颜色不对怎么定位llvmpipe做软件纹理采样时行为通常和真实GPU一致但某些扩展比如sRGB纹理、各向异性过滤可能没有真实硬件那么严格。颜色不对时先做两个减法写一个直接输出纯色纹理值的shader排除混合和shader逻辑用texelFetch跳过过滤和mipmap看原始纹素是否正确。如果texelFetch正确而texture不正确问题大概率出在纹理过滤参数或mipmap生成上。表格常见症状、可能原因、处理建议常见症状可能原因处理建议shader编译失败GLSL版本不匹配或函数签名错误检查#version对照规范签名打印shader日志texture采样全黑采样器未绑定/纹理坐标越界/mipmap未生成检查纹理对象绑定glGenerateMipmap尝试textureLod固定层级边缘闪烁或锯齿smoothstep边界条件错误 /fwidth在分支内使用确保edge0 edge1导数计算放分支外高光闪烁法线未归一化或mediump精度不足统一归一化关键向量提升到highp软渲染下颜色不对sRGB解码丢失/纹理格式错误用texelFetch对比检查sRGB扩展启用6.4 性能热点内置函数也分贵贱内置函数不是所有开销都一样。abs、min、max、clamp、mix、step、floor、fract这类基本函数通常映射到单条GPU指令几乎免费sin、cos、pow、atan2在部分移动GPU上可能需要几十甚至上百周期inverse、determinant尤其昂贵绝不应该在片段着色器里逐像素调用原子操作在竞争严重时也会拖慢整个工作组。实用优化套路包括用abs替代分支判断、用exp2替代pow(2.0, x)、用inversesqrt替代1.0 / sqrt、把逆矩阵计算提升到顶点阶段或CPU、把mod循环次数减少到最低。如果遇到一个shader在移动端明显卡顿先用性能分析器找出热点函数再逐项替换不要一上来就盲优化。6.5 总结一份自己的函数使用检查清单经过这些年的调试我给自己列了一份快速检查清单每次写shader前过一遍确认GLSL版本和当前着色器阶段查内置函数签名和重载版本注意edge0 edge1这类规范级约束确认导数函数不在非均匀控制流里检查纹理采样坐标类型是float还是ivec在真机和软渲染环境中各跑一次对比渲染器字符串和GLSL版本。这份清单看着琐碎但能省掉大量从结果反推原因的排查时间。说实话我是吃了很多亏才开始系统看spec的。内置函数这一章看起来只是一堆函数签名实际藏着OpenGL这套体系的很多设计哲学函数重载、精度约定、阶段限制、平台差异都浓缩在那些简练的描述里。后来我写shader的习惯也变了碰到不认识的函数先翻规范确认签名和版本再在真机上跑一遍检查渲染器字符串和GLSL版本最后才动手优化。这个顺序帮我少踩了很多坑。写这篇也算是对自己的一次总结整理希望也能帮到正在跟内置函数搏斗的人。
返回列表