webgl2 方法解析: layout - glsl 300 es版本

基本语法

layout 语法的基本形式如下:

scss 复制代码
layout(layout_qualifier) qualifier variable_name;
  • layout_qualifier :指定布局属性的值,例如 locationbinding 等。
  • qualifier :变量的类型,例如 inoutuniform 等。
  • variable_name:变量的名称。

版本支持情况

在webgl2 - glsl 300 es 版本中, layout 语法只支持 location 和 std140。

location

layout location语法 从 GLSL ES 300(对应 WebGL 2 / OpenGL ES 3.0)开始支持

location 用于指定顶点着色器的输入变量和片段着色器的输出变量的位置。这在顶点属性传递和片段输出时非常有用。

  • 顶点着色器中的输入变量

    ini 复制代码
    layout(location = 0) in vec3 vertexPosition;
    layout(location = 1) in vec3 vertexColor;

    这里,vertexPositionvertexColor 分别绑定到位置 0 和位置 1,对应 WebGL 中的 gl.VERTEX_ATTRIB_ARRAY

  • 片段着色器中的输出变量

    ini 复制代码
    layout(location = 0) out vec4 fragColor;

    这里,fragColor 绑定到片段输出位置 0,对应 WebGL 中的默认颜色附件。

layout location用法 示例

下方是一完整的可直接运行的例子, 可以从中体会layout location的用法

ini 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GLSL ES 3.00 layout</title>
    <style>
        canvas {
            width: 800px;
            height: 600px;
        }
    </style>
</head>
<body>
    <canvas id="glCanvas"></canvas>
    <script>
        //// 1. 获取上下文
        const canvas = document.getElementById('glCanvas');
        const gl = canvas.getContext('webgl2');
​
        if (!gl) {
            alert('WebGL2 不支持!');
            throw new Error('WebGL2 不支持!');
        }
​
        //// 2. 创建着色器
        // 顶点着色器 - 使用 layout(location)
        const vertexShaderSource = `#version 300 es
        layout(location = 0) in vec3 aPosition;
        
        void main() {
            gl_Position = vec4(aPosition, 1.0);
        }`;
​
        // 片段着色器 - 使用 layout(location)
        const fragmentShaderSource = `#version 300 es
        precision mediump float;
        layout(location = 0) out vec4 fragColor;
        
        void main() {
            fragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }`;
​
        // 创建着色器
        function createShader(gl, type, source) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);
            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error('着色器编译错误: ', gl.getSaderInfoLog(shader));
                gl.deleteShader(shader);
​
                return null;
            }
​
            return shader;
        }
​
        //// 3. 创建程序对象
        // 创建程序
        function createProgram(gl, vertexShader, fragmentShader) {
            const program = gl.createProgram();
            gl.attachShader(program, vertexShader);
            gl.attachShader(program, fragmentShader);
            gl.linkProgram(program);
            if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
                console.error('程序链接错误: ', gl.getProgramInfoLog(program));
                gl.deleteProgram(program);
                return null;
            }
​
            return program;
​
        }
​
        // 创建着色器和程序
        const vertexSader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
        const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
        const program = createProgram(gl, vertexSader, fragmentShader);
​
        //// 4. 使用程序
        // 使用程序
        gl.useProgram(program);
​
​
        //// 5. 传输属性
        // 创建顶点缓冲区
        const positions = [
            -0.5, -0.5, 0.0,
             0.5, -0.5, 0.0,
             0.0,  0.5, 0.0
        ];
        const positionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
​
        // 绑定顶点属性
        const positionAttribLoc = 0;    // 使用 layout(location = 0), 省略了原来的getAttribLocation()调用
        gl.enableVertexAttribArray(positionAttribLoc);
        gl.vertexAttribPointer(positionAttribLoc, 3, gl.FLOAT, false, 0, 0);
​
        //// 6. 绘制
        // 绘制
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLES, 0, 3);
    </script>
</body>
</html>

使用layout location 可省略 getAttribLocation()调用

GLSL 300 ES 及更高版本中,如果使用 layout(location = X) 显式指定顶点属性(attribute)的位置,就可以 省略 JavaScript 中的 gl.getAttribLocation() 调用,直接通过已知的 location 绑定属性。这是现代 GLSL 的特性之一,用于简化代码并提升可维护性。

std140

std140 在 GLSL ES 300(OpenGL ES 3.0 / WebGL 2)开始支持

std140 是 OpenGL 着色语言 (GLSL) 中定义的一种统一缓冲区对象 (UBO) 和着色器存储缓冲区对象 (SSBO) 的内存布局规范。它规定了数据在缓冲区中的精确排列方式,确保 CPU 和 GPU 对数据结构的解释一致。

基本规则

std140 布局遵循以下核心规则:

  1. 基础对齐规则

    • 标量类型 (bool, int, float):对齐到自身大小 (4字节)
    • 向量类型:对齐到 vec4 的大小 (16字节)
    • 数组:每个元素对齐到 vec4 (16字节)
    • 结构体:对齐到其最大成员的对齐值,且大小必须填充为对齐值的整数倍
    • 矩阵:视为列向量数组,每列对齐到 vec4
  2. 填充规则

    • 任何不符合对齐要求的情况都会自动插入填充字节

具体类型对齐

1. 标量类型
arduino 复制代码
layout(std140) uniform Example {
    float a;    // 偏移 0,占用 4 字节
    int b;      // 偏移 4,占用 4 字节
    bool c;     // 偏移 8,占用 4 字节
};              // 总大小 12 字节
2. 向量类型
less 复制代码
layout(std140) uniform Vectors {
    vec2 a;     // 偏移 0,占用 8 字节
    vec3 b;     // 偏移 16 (不是 8!),占用 12 字节
    vec4 c;     // 偏移 32 (不是 28!),占用 16 字节
};              // 总大小 48 字节

注意 vec3 会被填充到 16 字节边界。

3. 数组
scss 复制代码
layout(std140) uniform Arrays {
    float a[3]; // 每个元素 16 字节,总 48 字节
};              // 总大小 48 字节
4. 结构体
scss 复制代码
struct Light {
    vec3 position;  // 16 字节 (实际使用 12,填充 4)
    vec3 color;     // 16 字节 (实际使用 12,填充 4)
    float intensity;// 4 字节
};                  // 总大小 36 字节 (填充到 48)
​
layout(std140) uniform Lights {
    Light lights[2]; // 每个 Light 48 字节 (填充到 16 倍数)
};                   // 总大小 96 字节
5. 矩阵
scss 复制代码
layout(std140) uniform Matrices {
    mat3 m;     // 3 个 vec4 (48 字节)
    mat4 n;     // 4 个 vec4 (64 字节)
};              // 总大小 112 字节

实际应用注意事项

  1. 性能考虑:虽然 std140 确保了可预测的布局,但可能因填充而浪费内存。对于频繁更新的数据,应考虑内存使用效率。
  2. 跨平台兼容性:std140 保证了不同硬件/驱动上的一致行为。
  3. 调试技巧 :使用 glGetActiveUniformsiv 查询偏移量验证布局是否符合预期。

layout std140用法 示例

下方是一个完整的示例

ini 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GLSL ES 3.00 layout std140</title>
    <style>
      body {
        margin: 0;
      }
        canvas {
            display: block;
            width: 100vw;
            height: 100vh;
        }
    </style>
</head>
<body>
    <canvas id="glCanvas"></canvas>
    <script>
        //// 1. 获取渲染上下文
        const canvas = document.getElementById('glCanvas');
        const gl = canvas.getContext('webgl2');
​
        if (!gl) {
            alert('获取webgl2上下文失败!');
            throw new Error('获取webgl2上下文失败!');
        }
​
        //// 2. 创建着色器
        const vertexShaderSource = `#version 300 es
        precision highp float;
        
        layout(std140) uniform TransformBlock {
            mat4 modelMatrix;
            mat4 viewMatrix;
            mat4 projectionMatrix;
            vec4 color;
            float intensity;
        };
        
        in vec2 aPosition;
        out vec4 vColor;
        
        void main() {
            vColor = color * intensity;
            gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(aPosition, 0.0, 1.0);
        }`;
​
        const fragmentShaderSource = `#version 300 es
        precision mediump float;
        
        in vec4 vColor;
        out vec4 fragColor;
        
        void main() {
            fragColor = vColor;
        }`;
​
        function createShader(gl, type, source) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);
​
            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error('着色器编译错误: ', gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }
​
            return shader;
        }
​
        const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
        const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
​
        //// 3. 创建渲染程序
        function createProgram(gl, vertexShader, fragmentShader) {
            const program = gl.createProgram();
            gl.attachShader(program, vertexShader);
            gl.attachShader(program, fragmentShader);
            gl.linkProgram(program);
​
            if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
                console.error('程序链接错误: ', gl.getProgramInfoLog(program));
                throw new Error('程序链接错误!');
                return null;
            }
​
            return program;
        }
​
        const program = createProgram(gl, vertexShader, fragmentShader);
​
        //// 4. 设置属性
        const positions = new Float32Array([
            0.0, 0.5,   // 顶点1
            -0.5, -0.5, // 顶点2
            0.5, -0.5   // 顶点3
        ]);
​
        // 创建并绑定顶点缓冲区
        const vertexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
​
        const positionAttribLoc = gl.getAttribLocation(program, 'aPosition');
        gl.enableVertexAttribArray(positionAttribLoc);
        gl.vertexAttribPointer(positionAttribLoc, 2, gl.FLOAT, false, 0, 0);
​
        //// 5. 设置 uniform
        // uniform block 传值步骤 (1). 获取 uniform block 传值所需要的信息
        // 获取 TransformBlock 在 program 中的索引(就相当于在program中的位置, 可以通过这个索引找到该 uniform block)
        const uniformBlockIndex = gl.getUniformBlockIndex(program, 'TransformBlock');
        // 获取 std140 布局下的 uniform block大小
        const blockSize = gl.getActiveUniformBlockParameter(program, uniformBlockIndex, gl.UNIFORM_BLOCK_DATA_SIZE);
        console.log('Uniform block 大小: ', blockSize, '字节');
        // 获取 uniform 在 block 中的偏移量
        const uniformIndices = gl.getActiveUniformBlockParameter(program, uniformBlockIndex, gl.UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES);
        const offsets = gl.getActiveUniforms(program, uniformIndices, gl.UNIFORM_OFFSET);
        console.log('Uniform偏移量: ', offsets);
​
        // uniform block 传值步骤 (2). 创建 uniform buffer object, 用来保存数据
        const uniformBuffer = gl.createBuffer();
        gl.bindBuffer(gl.UNIFORM_BUFFER, uniformBuffer);
        /*********************下方一直到 bufferData()都是设置数据*******/
        // 创建 ArrayBuffer 来存储 uniform
        const buffer = new ArrayBuffer(blockSize);
        const view = new DataView(buffer);
        // 设置 modelMatrix (mat4 - 16个 float)
        const modelMatrix = new Float32Array([
            1.0, 0.0, 0.0, 0.0,
            0.0, 1.0, 0.0, 0.0,
            0.0, 0.0, 1.0, 0.0,
            0.0, 0.0, 0.0, 1.0
        ]);
        // 设置 viewMatrix (mat4)
        const viewMatrix = new Float32Array([
            1.0, 0.0, 0.0, 0.0,
            0.0, 1.0, 0.0, 0.0,
            0.0, 0.0, 1.0, 0.0,
            0.0, 0.0, 0.0, 1.0
        ]);
        // 设置 projectionMatrix (mat4)
        const aspect = canvas.width / canvas.height;
        const projectionMatrix = new Float32Array([
            1.0/aspect, 0.0, 0.0, 0.0,
            0.0, 1.0, 0.0, 0.0,
            0.0, 0.0, 1.0, 0.0,
            0.0, 0.0, 0.0, 1.0
        ]);
        // 设置 color (vec4)
        const color = [Math.random(), Math.random(), Math.random(), 1.0];
        // 设置 intensity (float)
        const intensity = 0.5 + Math.random() * 0.5;
        // 将数据写入 buffer
        // modelMatrix (偏移量 0)
        modelMatrix.forEach((val, i) => {
            view.setFloat32(offsets[0] + i * 4, val, true);
        });
        // viewMatrix (偏移量 64, 因为 mat4 占用 64 字节)
        viewMatrix.forEach((val, i) => {
            view.setFloat32(offsets[1] + i * 4, val, true);
        });
        //projectionMatrix (偏移量 128)
        projectionMatrix.forEach((val, i) => {
            view.setFloat32(offsets[2] + i * 4, val, true);
        });
        // color (偏移量 192)
        color.forEach((val, i) => {
            view.setFloat32(offsets[3] + i * 4, val, true);
        });
        // intensity (偏移量 208)
        view.setFloat32(offsets[4], intensity, true);
        // 将上面设置的数据传入uniformbuffer中
        gl.bufferData(gl.UNIFORM_BUFFER, view, gl.DYNAMIC_DRAW);
​
        // uniform block 传值步骤 (3). 将program中的TransformBlock 与 上面创建的UBO绑定起来, 通过绑定点
        const bindingPoint = 0;
        // 将program中的TransformBlock绑定到绑定点
        gl.uniformBlockBinding(program, uniformBlockIndex, bindingPoint); 
        // 将UBO绑定到绑定点
        gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPoint, uniformBuffer);
​
        // 调整画布大小
        function resizeCanvas() {
            canvas.width = canvas.clientWidth;
            canvas.height = canvas.clientHeight;
            gl.viewport(0, 0, canvas.width, canvas.height);
        }
​
        // 6.渲染循环
        function render() {
            resizeCanvas();
            gl.clearColor(0.1, 0.1, 0.1, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
​
            gl.useProgram(program);
            gl.drawArrays(gl.TRIANGLES, 0, 3);
​
            requestAnimationFrame(render);
        }
​
        render();
​
    </script>
</body>
</html>

300 es版本中 uniform block 前面必须指定布局限定符

在 GLSL 300 ES (OpenGL ES Shading Language version 3.00) 中,uniform block 前面必须指定布局限定符,这是与旧版本 GLSL 的一个重要区别。

布局限定符要求

  1. 强制要求

    • 在 GLSL 300 ES 中,所有的 uniform block 都必须使用 layout 限定符显式声明其布局方式(300 es只支持 std140), 所以没有默认的布局方式, 因为必须显式声明布局方式
    • 不允许省略布局限定符(这与 GLSL 1.00/1.10 不同)
  2. 可用布局选项

    scss 复制代码
    layout(std140) uniform MyBlock { ... };  // 显式 std140 布局

如果没有指定布局会发生什么?

如果省略 layout 限定符,GLSL 300 ES 编译器会报错。例如:

scss 复制代码
// 错误写法(GLSL 300 ES 中不合法)
uniform MyBlock {
    vec4 color;
    float size;
};
​
// 正确写法(必须指定布局)
layout(std140) uniform MyBlock {
    vec4 color;
    float size;
};

为什么这样设计?

GLSL 300 ES 强制要求显式布局声明有几个原因:

  1. 可预测性:确保不同硬件/驱动上的一致行为
  2. 性能优化:让开发者明确选择内存布局策略
  3. 与现代GLSL规范对齐:与桌面版GLSL保持一致

绑定点与缓冲对象的关系

在 WebGL2 中,Uniform 块绑定点(Uniform Block Binding Point)Uniform 缓冲对象(Uniform Buffer Object, UBO) 共同构成了高效传递 Uniform 数据的机制。以下是它们的详细关系和区别:


1. Uniform 块绑定点(Binding Point)

定义

  • 绑定点是一个 全局的、整数索引的槽位(类似"插槽"),用于关联:

    1. 着色器程序中的 Uniform 块 (GLSL 中定义的 uniform BlockName { ... }
    2. Uniform 缓冲对象(UBO) (存储实际数据的 GPU 缓冲区)。
  • 绑定点是 WebGL2 上下文共享的,不同程序可以访问相同的绑定点。

作用

  • 解耦 Uniform 块和具体缓冲区,允许动态切换 UBO 数据,而无需重新链接着色器程序。

绑定流程

  1. 获取 Uniform 块的索引

    ini 复制代码
    const blockIndex = gl.getUniformBlockIndex(program, "LightBlock");
  2. 将 Uniform 块绑定到某个绑定点 (例如 0):

    ini 复制代码
    gl.uniformBlockBinding(program, blockIndex, 0);
  3. 将 UBO 绑定到相同的绑定点

    ini 复制代码
    gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uboBuffer);

2. Uniform 缓冲对象(UBO)

定义

  • UBO 是一块 GPU 缓冲区 (通过 gl.createBuffer() 创建),用于存储 Uniform 数据(如矩阵、光照参数等)。
  • 数据布局需与 GLSL 中的 std140 等规则匹配。

特点

  • 高效更新大量 Uniform 数据 (避免逐字段调用 gl.uniformXXX())。
  • 多个程序共享同一 UBO(通过绑定点)。

3. 绑定点与 UBO 的关系

类比:插槽与电池

  • 绑定点 类似设备上的电池插槽(如 插槽0插槽1)。
  • UBO 类似电池,可以插入任意插槽。
  • Uniform 块 类似设备电路,需要声明它使用哪个插槽的电源。

关键关系

  1. 绑定点是桥梁

    • 着色器通过 uniformBlockBinding 声明:"我要用绑定点 X 的数据"。
    • 开发者通过 bindBufferBase 将 UBO 绑定到 X,完成数据关联。
  2. 动态切换

    • 可以随时更换绑定点 X 对应的 UBO,无需修改着色器代码。

    • 例如:

      scss 复制代码
      // 切换 UBO(同一绑定点)
      gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uboBuffer1); // 使用数据1
      gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uboBuffer2); // 切换为数据2

4. 完整流程示例

GLSL 代码(GLSL 300 ES)

ini 复制代码
#version 300 es
layout(std140) uniform LightBlock { // 未指定 binding,需通过 JS 绑定
    vec3 lightColor;
    float intensity;
};

JavaScript 代码

ini 复制代码
// 1. 创建 UBO 并填充数据
const uboBuffer = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, uboBuffer);
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array([1, 1, 0, 2.0]), gl.STATIC_DRAW);
​
// 2. 获取 Uniform 块索引
const blockIndex = gl.getUniformBlockIndex(program, "LightBlock");
​
// 3. 将 Uniform 块绑定到绑定点 0
gl.uniformBlockBinding(program, blockIndex, 0);
​
// 4. 将 UBO 绑定到绑定点 0
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uboBuffer);

5. 常见问题

Q: 为什么需要绑定点?直接关联 UBO 和 Uniform 块不行吗?

  • 解耦设计:绑定点允许 UBO 和 Uniform 块动态关联,避免重新编译着色器。
  • 资源共享:多个程序可通过同一绑定点共享 UBO(如全局光照数据)。

Q: 绑定点数量有限制吗?

  • 可通过 gl.getParameter(gl.MAX_UNIFORM_BUFFER_BINDINGS) 查询(通常 ≥ 12)。

总结

概念 作用
Uniform 块绑定点 全局槽位,关联 Uniform 块和 UBO。
Uniform 缓冲对象 存储 Uniform 数据的 GPU 缓冲区。
关系 绑定点是"插槽",UBO 是"电池",Uniform 块声明自己要用哪个插槽的电源。

这种设计大幅提升了 Uniform 数据管理的灵活性和性能! 🚀

在GLSL 310 ES版本支持 binding语法后, 可省略 gl.uniformBlockBinding()的调用, 但是在WEBGL2中不支持

相关推荐
Antonio9157 分钟前
【网络编程】WebSocket 实现简易Web多人聊天室
前端·网络·c++·websocket
德育处主任Pro1 小时前
p5.js 用 beginGeometry () 和 endGeometry () 打造自定义 3D 模型
开发语言·javascript·3d
tianzhiyi1989sq1 小时前
Vue3 Composition API
前端·javascript·vue.js
今禾1 小时前
Zustand状态管理(上):现代React应用的轻量级状态解决方案
前端·react.js·前端框架
用户2519162427112 小时前
Canvas之图形变换
前端·javascript·canvas
今禾2 小时前
Zustand状态管理(下):从基础到高级应用
前端·react.js·前端框架
gnip2 小时前
js模拟重载
前端·javascript
Naturean2 小时前
Web前端开发基础知识之查漏补缺
前端
curdcv_po2 小时前
🔥 3D开发,自定义几何体 和 添加纹理
前端
单身汪v2 小时前
告别混乱:前端时间与时区实用指南
前端·javascript