图形学【免费下载链接】webgl-fundamentalsWebGL lessons that start with the basics项目地址https://gitcode.com/gh_mirrors/we/webgl-fundamentals点击查看免费下载本文基于 webgl-fundamentals 仓库的问答教程《How to bind an array of textures to a WebGL shader uniform?》展开。当你需要让大量几何体共享少量纹理、希望用顶点属性vertex attribute为每个顶点/片元动态挑选纹理而不是为每张纹理手写一个 uniform sampler 时本指南将给出可直接落地的 GLSL 声明方式、JavaScript 侧的gl.uniform1iv绑定方法以及完整的可运行示例。读完你不仅能正确使用采样器数组还会彻底理解纹理单元texture units在“纹理 → 采样器”链路中的角色以及它与纹理图集texture atlas方案的取舍。问题背景当每个顶点选一张纹理的需求撞上 GLSL 限制在常见的 WebGL 开发场景中我们可能有一批对象它们共享同一批例如 4 张纹理。最直觉的做法是每个对象手写一个纹理采样流程例如在片段着色器里写死uniform sampler2D u_texture0; uniform sampler2D u_texture1; ...然后写一堆switch/if分支去选择用哪一张。但这样的代码很快会失控纹理数量一变着色器就要重写、重新编译分支逻辑和纹理索引完全写死在 shader 里无法由数据驱动WebGL 的 GLSL 中没有switch (index) { case ... }这类运行时分支语句无法用传入一个索引、按索引取纹理这种直接思路。于是自然的想法是把该用哪张纹理作为一个顶点属性vertex attribute传给顶点着色器再把它作为索引在片段着色器中从一个纹理数组里取值。但 OpenGL Wiki 关于 Sampler 的说明给出了一条看似劝退的规则A variable of sampler can only be defined in one of two ways. It can be defined as a function parameter or as a uniform variable.uniform sampler2D texture1;这听起来像是采样器不能组成数组。实际上采样器可以声明为数组uniform sampler2D u_textures[4];只是WebGL 1GLSL ES 1.00不允许用运行时非编译期常量表达式去动态索引采样器数组。这正是本文要解决的核心矛盾既要用数组 索引的思路又必须满足采样器只能被常量表达式索引的约束。方案总览采样器数组 常量索引循环 逐单元绑定官方教程给出的解法可以拆成三个层次GLSL 侧声明uniform sampler2D u_textures[N];数组用#define定义数组大小GLSL 侧的关键技巧不能直接texture2D(u_textures[ndx], uv)ndx是运行时值而是用编译期可解析的循环索引遍历整个数组再与传入的运行时索引比较命中才累加JavaScript 侧通过gl.getUniformLocation拿到数组 uniform 的位置再用gl.uniform1iv(textureLoc, [0, 1, 2, 3])一次性告诉 shader数组第 i 个元素对应第 i 号纹理单元随后用gl.activeTexture(gl.TEXTURE0 i)把实际纹理绑定到对应单元。下面逐层拆解。第一步在片段着色器中声明并使用采样器数组教程中的片段着色器核心代码如下#define numTextures 4 precision mediump float; varying float v_textureIndex; uniform sampler2D u_textures[numTextures]; vec4 getSampleFromArray(sampler2D textures[4], int ndx, vec2 uv) { vec4 color vec4(0); for (int i 0; i numTextures; i) { vec4 c texture2D(u_textures[i], uv); if (i ndx) { color c; } } return color; } void main() { gl_FragColor getSampleFromArray(u_textures, int(v_textureIndex), vec2(0.5, 0.5)); }几个值得注意的细节#define numTextures 4是预处理宏它让数组长度和循环次数两处保持一致后续要改成 8 张纹理只需改这一处varying float v_textureIndex是顶点着色器插值传入的纹理索引在main()中用int(v_textureIndex)转成整数函数参数sampler2D textures[4]印证了 OpenGL Wiki 的说法——采样器可以作为函数参数传入规避动态索引的关键texture2D(u_textures[i], uv)中的下标i是for循环的循环索引。在 GLSL ES 1.00 中循环索引属于常量索引表达式constant-index-expression的合法成员因此这种写法是合法的而运行时变量ndx只参与if (i ndx)的比较从不直接作为采样器数组下标——这就绕开了采样器数组只能被常量表达式索引的限制效率上该写法每次采样会遍历并采样全部 4 张纹理color c只把命中项加进去。对本教程的演示场景没有问题但若要追求极致性能可考虑后面介绍的纹理图集方案。顶点着色器一侧同样简单把a_textureIndex属性直接透传给varyingattribute vec4 a_position; attribute float a_textureIndex; varying float v_textureIndex; void main() { gl_Position a_position; v_textureIndex a_textureIndex; }完整代码可见仓库中的可运行示例 webgl-qna-how-to-bind-an-array-of-textures-to-a-webgl-shader-uniform--example-1.html。注意该示例在调用函数时写的是int(v_textureIndex 0.5)——因为int()是截断取整加 0.5 是常用的四舍五入小技巧避免插值后的浮点误差比如 2.9999截断成错误索引。第二步用 gl.uniform1iv 把采样器数组映射到纹理单元光在着色器里声明数组还不够。采样器本身不保存纹理数据它保存的是一个纹理单元编号整数。因此 JavaScript 侧要做的是为采样器数组整体赋值一组纹理单元编号。var textureLoc gl.getUniformLocation(program, u_textures); // Tell the shader to use texture units 0 to 3 gl.uniform1iv(textureLoc, [0, 1, 2, 3]);说明gl.getUniformLocation(program, u_textures)直接以数组名查询位置是可行的得到的 location 指向整个数组仓库示例 webgl-qna-how-to-bind-an-array-of-textures-to-a-webgl-shader-uniform--example-1.html 中写的是u_textures[0]这也是一种常见写法——查询数组首元素的位置效果等价gl.uniform1iv是设置整型数组 uniform的 API它正是 WebGL Shaders and GLSL 教程 里列出的gl.uniform1iv(sampler2DUniformLoc, [v])用法对采样器数组设置纹理单元时传的数值就是纹理单元编号[0, 1, 2, 3]意味着u_textures[0]对应纹理单元 0u_textures[1]对应纹理单元 1以此类推。这里的下标顺序必须与后面gl.activeTexture的绑定顺序严格一致。第三步理解纹理单元——采样器与真实纹理之间的桥梁为什么上面要强调纹理单元编号这需要理解 WebGL 的纹理单元机制。相关教程 WebGL Texture Units 给出了非常直观的模型纹理单元本质上是WebGL 全局状态中的一个纹理引用数组如果 WebGL 用 JavaScript 实现其内部状态大致长这样const gl { activeTextureUnit: 0, textureUnits: [ { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, }, { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, }, // ... 每个单元都有 TEXTURE_2D 和 TEXTURE_CUBE_MAP 两个绑定点 ]; }而gl.activeTexture、gl.bindTexture的作用相当于gl.activeTexture function(unit) { gl.activeTextureUnit unit - gl.TEXTURE0; // 转换为从 0 开始的下标 }; gl.bindTexture function(target, texture) { const textureUnit gl.textureUnits[gl.activeTextureUnit]; textureUnit[target] texture; };因此绑定纹理分两步先用gl.activeTexture(gl.TEXTURE0 n)选中第 n 号单元再用gl.bindTexture(gl.TEXTURE_2D, tex)把纹理挂到该单元的TEXTURE_2D绑定点上。之后 shader 里采样器值纹理单元编号就是去第 n 号单元里取我绑定的那张纹理。这正是 WebGL Using 2 or More Textures 教程 反复强调的两条配套动作gl.uniform1i(u_image0Location, 0); // texture unit 0 gl.uniform1i(u_image1Location, 1); // texture unit 1以及gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[0]); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, textures[1]);由于gl.TEXTURE0、gl.TEXTURE1……这些常量是连续的可以写成循环for (var ii 0; ii 4; ii) { gl.activeTexture(gl.TEXTURE0 ii); gl.bindTexture(gl.TEXTURE_2D, textures[ii]); }完整可运行示例四个单像素纹理拼一个方块仓库中给出了可直接运行的完整示例 webgl-qna-how-to-bind-an-array-of-textures-to-a-webgl-shader-uniform--example-1.html。它在 300×300 的画布上绘制一个矩形六个顶点分别携带纹理索引 0、1、2、3、0、1从而把四个 1×1 的纯色纹理按三角形分布渲染出来。整体流程如下。1. 编译着色器程序var program webglUtils.createProgramFromScripts( gl, [vshader, fshader], [a_position, a_textureIndex]); gl.useProgram(program);这里用到了仓库自带的工具函数createProgramFromScripts定义于 webgl/resources/webgl-utils.js。从源码可见它会先通过createShaderFromScript按[VERTEX_SHADER, FRAGMENT_SHADER]的顺序编译两个script标签里的着色器再调用createProgram完成 attach、link 与useProgram传入的第三个参数[a_position, a_textureIndex]会被逐个执行gl.bindAttribLocation(program, ndx, attrib)即按数组下标 0、1 绑定属性位置省去手动查询getAttribLocation的样板代码。2. 设置采样器数组对应的纹理单元var textureLoc gl.getUniformLocation(program, u_textures[0]); // Tell the shader to use texture units 0 to 3 gl.uniform1iv(textureLoc, [0, 1, 2, 3]);3. 准备顶点数据位置 纹理索引属性var positions [ 1, 1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, ]; var textureIndex [ 0, 1, 2, 3, 0, 1, ];纹理索引使用Uint8Array上传既省内存相比 Float32Array 每个值只占 1 字节又天然是整数配合片元着色器里的int(v_textureIndex 0.5)取整逻辑非常合适var vertBuffer gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(textureIndex), gl.STATIC_DRAW); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 1, gl.UNSIGNED_BYTE, false, 0, 0);注意gl.vertexAttribPointer的type参数为gl.UNSIGNED_BYTE对应attribute float a_textureIndex会自动把无符号字节规范化为 0.0~1.0 的浮点数之后由 varying 插值进入片段着色器。4. 创建四张纹理并绑定到单元 0~3var colors [ [0, 0, 255, 255], // 蓝 [0, 255, 0, 255], // 绿 [255, 0, 0, 255], // 红 [0, 255, 255, 255], // 青 ]; colors.forEach(function(color, ndx) { gl.activeTexture(gl.TEXTURE0 ndx); var tex gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(color)); });这里的关键点gl.activeTexture(gl.TEXTURE0 ndx)与前面gl.uniform1iv(textureLoc, [0, 1, 2, 3])形成一一对应——采样器数组第 ndx 个元素使用的纹理单元编号是 ndx而纹理单元 ndx 里绑定的正是第 ndx 张纯色纹理。若两者顺序错位画面颜色就会张冠李戴。5. 绘制gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);由于使用的是纹理单元编号整数而不是纹理对象本身同一张纹理完全可以同时挂到多个采样器上这也是许多对象共享少量纹理场景的关键能力。为什么不能在 WebGL 1 里动态索引采样器数组这是本方案最容易被误解的技术点。GLSL ES 1.00WebGL 1 的着色器语言规定采样器数组只能用常量索引表达式constant-index-expression访问即编译期就能确定的下标。像texture2D(u_textures[ndx], uv)ndx来自 varying 或 uniform 的运行时值这类写法在多数实现上会直接导致编译失败或未定义行为。教程给出的遍历 比较写法正是为满足这一约束而设计的for (int i 0; i numTextures; i) { vec4 c texture2D(u_textures[i], uv); // i 是循环索引属于常量索引表达式 if (i ndx) { color c; // ndx 只参与比较不参与数组索引 } }代价是每片元会对全部 N 张纹理采样。对于纹理数量少、片元数量大的场景这会显著放大采样开销——这也是教程作者在文末主动抛出纹理图集替代方案的原因。与纹理图集texture atlas方案的取舍原问答在 PS 中明确询问如果纹理图集更高效或更简单欢迎分享经验。仓库教程 WebGL 3D Textures 恰好给出了完整对比其核心结论是对一个模型的不同面用不同纹理这类需求纹理图集通常是更优解。纹理图集是把多张图片拼进同一张大纹理通过给每张图分配不同的纹理坐标UV区间来选中对应区域。比如把 6 张图片放进一张纹理再用类似下面的坐标选取不同子图// select the top left image 0 , 0 , 0 , 0.5, 0.25, 0 , 0 , 0.5, 0.25, 0.5, 0.25, 0 ,该教程明确指出它的三大优势只有 1 张纹理要加载shader 保持简单只需引用 1 个采样器只需要 1 次 draw call 就能绘制整个形状而不是每个纹理一次这正是几乎所有高性能应用尤其是游戏采用的做法。对应的示例可见 webgl-3d-textures-texture-atlas.html。两种方案的选择建议纹理图集适合同一几何体/模型的不同部位用不同贴图一次 draw call、单采样器性能与代码复杂度都最优采样器数组适合大量对象共享一小撮纹理、每对象/每顶点动态指定纹理尤其是纹理来源是运行时动态创建例如程序化生成、离屏渲染结果、多张 canvas/视频源而难以拼进图集的情况。此时用gl.uniform1iv一次性绑定整个数组配合顶点属性做索引代码组织比写 N 个独立 sampler 干净得多。数量上限与设备兼容性提醒纹理单元不是无限的。教程 WebGL Texture Units 给出了重要的容量检查方法const maxTextureUnits gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); const maxVertexShaderTextureUnits gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); const maxFragmentShaderTextureUnits gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);要点WebGL 规范要求实现至少支持 8 个纹理单元MAX_COMBINED_TEXTURE_IMAGE_UNITS顶点着色器与片段着色器的单元上限可能不同且共用组合上限例如组合上限为 8、顶点着色器占用了 2 个单元时片段着色器只剩 6 个可用MAX_VERTEX_TEXTURE_IMAGE_UNITS甚至可能返回 0——即部分设备完全不支持在顶点着色器里采样纹理该情况虽然罕见但存在。若计划在顶点着色器中使用纹理务必先查询该参数并做降级处理。对本文的片段着色器方案而言只要u_textures的数组长度不超过MAX_TEXTURE_IMAGE_UNITS即可。若纹理数量较大建议在初始化时读取该参数并动态确定数组长度。小结三个环节的对应关系把本文的完整链路串起来核心是三层一一对应环节代码作用GLSL 声明uniform sampler2D u_textures[4];声明采样器数组长度用#define宏控制JS 绑定单元编号gl.uniform1iv(loc, [0, 1, 2, 3]);告诉 shader 数组第 i 个采样器用第 i 号纹理单元JS 绑定真实纹理gl.activeTexture(gl.TEXTURE0 i); gl.bindTexture(...)把实际纹理挂到对应纹理单元三者任一环节的索引错位都会导致取样错乱。此外用顶点属性携带纹理索引、在片元着色器里用常量循环索引采样 运行时索引比较绕过动态索引限制是本方案能在 WebGL 1 下成立的关键。更深入地了解纹理与纹理单元可继续阅读仓库内的 WebGL 3D Textures、WebGL Using 2 or More Textures、WebGL Texture Units 以及 WebGL Shaders and GLSL 系列教程。赞分享图形学【免费下载链接】webgl-fundamentalsWebGL lessons that start with the basics项目地址https://gitcode.com/gh_mirrors/we/webgl-fundamentals点击查看免费下载相关推荐WebGL 纹理单元Texture Units完全指南从全局数组到着色器采样器绑定WebGL 纹理单元Texture Units完全指南从全局数组到着色器采样器绑定 纹理单元Texture Units是 WebGL 中连接“纹理数据图形学WebGL 多纹理2 个及以上纹理实战纹理单元与单次绘制调用中的多纹理采样WebGL 多纹理2 个及以上纹理实战纹理单元与单次绘制调用中的多纹理采样 本指南基于 webgl fundamentals 仓库的 WebGL Usin图形学WebGL 纹理贴图实战指南从着色器采样到纹理图集webgl-fundamentals 系列WebGL 纹理贴图实战指南从着色器采样到纹理图集webgl fundamentals 系列 本指南以 webgl fundamentals https:图形学创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考