Quiet
  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我

bajiu

  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我
Quiet主题
  • WebGL

webGL基础

bajiu
前端

2022-11-01 17:22:00

1993年有一款第一人称设计游戏《Doom》,是当时DOS系统下具有里程碑意义的第一人称设计游戏,虽然使用的是2.5D的技术,但成功的创造了一个具有深度干的三维空间,从游戏玩家的视角上看,Doom当时引入了一些新的游戏特性,比如高度差和地形差,墙壁再也不是吃豆豆那样垂直的了,所有的表面都有了材质贴图,以及光线的层次和阴影。

早期《Doom》对图形技术的贡献和影响:

  • 光栅化技术:早期的《Doom》使用了一种称为光栅化的渲染技术,结合了2D精灵和3D空间中的平面。这种方法使得游戏能够在硬件有限的情况下实现相对复杂的3D环境。
  • 软件渲染:游戏使用了软件渲染,计算图形效果(如阴影、光照)依赖于CPU而非GPU,这在当时是比较先进的做法。
  • 分层技术:通过使用分层图层和贴图技术,《Doom》能够在不需要真正的3D模型的情况下创造出深度感和复杂性。这种设计为后来的图形引擎发展奠定了基础。
  • 引擎创新:Doom引擎开创了很多后来的图形引擎设计理念,包括视锥体剔除、门和楼梯的实现等。这些技术为后续使用硬件加速和着色器的图形开发奠定了基础。

虽然早期的Doom没有直接使用GLSL(当时也没那个玩意),但在图形渲染和游戏引擎设计上为后来的发展铺平了道路,随着技术演变,最终变成了我们前端工程师用的现代图形编程中的着色器语言。这篇我们先只介绍在web端的webGL.

什么是WebGL

WebGL是一种3D绘图标准,这种绘图技术标准允许把JavaScript和OpenGL ES结合在一起,通过增加OpenGL ES的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染(部分计算GPU),所以WebGL的本质就是:JavaScript操作OpenGL接口。

OpenGL ES 概念

既然上面已经说了,webGL是js操作OpenGL的接口,那我们就直接从OpenGL ES开始,我们先介绍几个关于OpenGL中的概念,因为主要是在web中实践,所以主要基于OpenGL ES 3.0版本接口,关于这个后面会说。

上下文(Context)

上下文是一个图形API(OpenGL、WebGL或DirectX)中的核心概念,代表着一组状态和资源,类似一个状态机(维护运行时)。上下文管理着GPU的状态,包括纹理、缓冲区、着色器程序等,是渲染操作的核心,包括:

  • 状态信息:如深度测试、混合模式、剔除模式等。
  • 资源管理:包括纹理、帧缓冲、顶点缓冲等对象的管理。
  • 绘制命令:所有的绘制调用(如绘制三角形、线条等)都通过上下文来发起。

上下文可以视为与GPU进行交互的接口,创建后需要在渲染过程中保持有效。

缓冲区(FrameBuffer)

帧缓冲区是用于存储渲染结果的内存区域,通常由多种附件组成,包括颜色缓冲区、深度缓冲区和模板缓冲区。帧缓冲区的类型包括:

  • 默认帧缓冲区:系统提供的帧缓冲区,通常用于直接在屏幕上渲染。
  • 离屏帧缓冲区:用户创建的帧缓冲区,允许在不直接显示到屏幕的情况下进行渲染,适用于后处理效果。

帧缓冲区的附件通常通过附着(Attachment)的方式来进行配置,以便在渲染时可以将不同的图像数据写入不同的缓冲区。

附着(Attachment)

附着是将图像数据(如颜色、深度、模板)附加到帧缓冲区的过程。常见的附着类型包括:

  • 颜色附着:存储每个像素的颜色信息,通常使用一个或多个颜色缓冲区。
  • 深度附着:存储每个像素的深度值,用于深度测试,确保遮挡关系正确。
  • 模板附着:用于模板测试,允许进行复杂的遮挡效果,如阴影映射。

在OpenGL中,附着操作通常使用glFramebufferTexture或glFramebufferRenderbuffer函数完成。

顶点数组(VertexArray)和顶点缓冲区(VertexBuffer)

  • 顶点数组(VertexArray):在OpenGL中,顶点数组对象(VAO)用于封装顶点属性的状态,包括顶点缓冲区的绑定状态、顶点属性指针设置等。VAO使得开发者能够方便地管理顶点数据。
  • 顶点缓冲区(VertexBuffer):用于实际存储顶点数据的对象,通常以GPU内存的形式存在。每个顶点缓冲区可以包含多种属性(如位置、法线、纹理坐标等),并通过glBindBuffer和glBufferData等函数进行管理。

索引数组(ElementArray) 和索引缓冲区(ElementBuffer)

  • 索引数组(ElementArray):用于定义图形的构建顺序,通过索引来指定顶点,减少数据冗余。可以使得同一个顶点被多次引用,节省内存。
  • 索引缓冲区(ElementBuffer):实际存储索引数据的对象。索引缓冲区通过glBindBuffer和glBufferData函数创建,并通过glDrawElements调用时使用。

纹理(Texture)、渲染缓冲区(RenderBuffer)

  • 纹理(Texture):是GPU用于存储图像数据的对象,允许在渲染过程中将图像映射到物体表面。纹理可以是2D、3D,甚至是立方体纹理,支持多种过滤和包裹模式。
  • 渲染缓冲区(RenderBuffer):主要用于离屏渲染,不可以作为纹理使用。适用于存储深度和模板数据,通常具有更高的性能,适合快速的渲染操作。

着色器程序

着色器程序是GPU上的小型程序,负责在渲染过程中处理图形数据。常见的着色器包括:

1.顶点着色器:处理每个顶点的数据,主要任务包括:

  • 接收顶点属性(如位置、法线等)。
  • 进行变换(如模型、视图和投影变换)。
  • 计算光照和阴影等信息。
  • 将顶点的最终位置传递给光栅化阶段。

2.片元着色器:处理每个片元(即将要显示的像素),主要任务包括:

  • 接收从顶点着色器传来的插值数据(如纹理坐标、颜色等)。
  • 计算片元的最终颜色,包括纹理采样和光照计算。
  • 进行各种效果的处理,如透明度、混合等。

图形管线(Graphics Pipeline)

图形管线是将输入数据(如顶点和纹理)转换为最终图像的一系列处理步骤。WebGL的图形管线通常包括以下主要阶段:

  1. 顶点处理(Vertex Processing):在此阶段,顶点着色器被执行。每个顶点的属性(如位置、法线、颜色等)通过着色器进行处理,进行坐标变换(如模型变换、视图变换、投影变换),并生成屏幕坐标。
  2. 光栅化(Rasterization):将处理后的顶点转换为片元(fragment)。每个片元代表屏幕上的一个像素。光栅化还会决定片元的颜色、深度等属性。
  3. 片元处理(Fragment Processing):在此阶段,片元着色器被执行。每个片元的颜色和其他属性通过片元着色器进行计算,通常涉及纹理采样和光照计算。
  4. 输出合并(Output Merging):将片元的最终颜色与帧缓冲区中的现有颜色进行合并,通常涉及深度测试和混合(blending)操作。

图形管线的意义在于将图形渲染的各个步骤模块化,允许开发者在特定阶段插入自定义的着色器代码,控制渲染过程的细节。也使得现代GPU能够高度并行化处理,因为每个阶段可以同时处理多个顶点和片元,大大提高了渲染性能。

WebGL渲染流程

上面有了基础概念之后,下面就是渲染流程,这里我们暂时只考虑webGL2哈:

1.创建WebGL上下文:

创建一个WebGL上下文,这个上下文用于与GPU交互。

    // 创建WebGL上下文
    const canvas = document.getElementById("glCanvas");
    const gl = canvas.getContext("webgl");

    if (!gl) {
        console.error("无法初始化WebGL上下文");
    }

2.设置视口和清除颜色:

  • 使用gl.viewport设置视口的大小和位置。
  • 使用gl.clearColor设置背景色,使用gl.clear清空颜色缓冲区。
    // 设置视口和清除颜色
    gl.viewport(0, 0, canvas.width, canvas.height); // 设置视口大小
    gl.clearColor(0.0, 0.0, 0.0, 1.0); // 设置清除颜色为黑色
    gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区

3.加载和编译着色器:

  • 创建顶点着色器和片元着色器,并将其源代码传递给WebGL。
  • 编译着色器并检查编译是否成功。

    // 定义顶点着色器的源代码
    const vsSource = `#version 300 es
        in vec4 aVertexPosition; // 顶点位置属性
        in vec4 aVertexColor;    // 顶点颜色属性
        out vec4 vColor;         // 传递到片元着色器的颜色
        void main(void) {
            gl_Position = aVertexPosition; // 设置顶点位置
            vColor = aVertexColor;         // 传递颜色到片元着色器
        }
    `;

    // 定义片元着色器的源代码
    const fsSource = `#version 300 es
        precision mediump float; // 设置浮点数精度
        in vec4 vColor;          // 从顶点着色器接收颜色
        out vec4 fragColor;      // 输出片元颜色
        void main(void) {
            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)) {
            return shader; // 返回编译成功的着色器
        } else {
            console.error(gl.getShaderInfoLog(shader)); // 输出错误信息
            gl.deleteShader(shader); // 删除着色器
        }
    }

4.创建着色器程序:

  • 将编译好的顶点着色器和片元着色器附加到一个程序对象上,并链接程序。
    // 创建着色器程序
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); // 创建顶点着色器
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); // 创建片元着色器

    // 创建着色器程序
    const shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader); // 附加顶点着色器
    gl.attachShader(shaderProgram, fragmentShader); // 附加片元着色器
    gl.linkProgram(shaderProgram); // 链接程序

     // 使用着色器程序
    gl.useProgram(shaderProgram);

5.创建和绑定缓冲区:

  • 创建顶点缓冲区(Vertex Buffer)和颜色缓冲区(Color Buffer),并将顶点和颜色数据传入这些缓冲区。
  • 绑定缓冲区,使其成为当前操作的缓冲区。
    // 顶点
    const vertices = new Float32Array([
        0.0,  1.0,  
        -1.0, -1.0,
        1.0, -1.0  
    ]);

    // 顶点缓冲区
    const vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定顶点缓冲区
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 将顶点数据传入缓冲区

    // yanse
    const colors = new Float32Array([
        1.0, 0.0, 0.0, 1.0, 
        0.0, 1.0, 0.0, 1.0, 
        0.0, 0.0, 1.0, 1.0 
    ]);
    
    // 颜色缓冲区
    const colorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); // 绑定颜色缓冲区
    gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW); // 将颜色数据传入缓冲区

6.设置顶点属性指针:

  • 获取顶点位置和颜色属性的位置,并指定如何从缓冲区中读取数据。

    // 拿一下顶点位置属性指针地址
    const positionLocation = gl.getAttribLocation(shaderProgram, "aVertexPosition");
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定顶点缓冲区
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); // 指定顶点属性
    gl.enableVertexAttribArray(positionLocation); // 启用顶点属性数组

7.绘制图形:

  • 使用gl.drawArrays或gl.drawElements发起绘制命令,告诉WebGL如何使用顶点数据来绘制图形。
    gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区
    gl.drawArrays(gl.TRIANGLES, 0, 3); // 浅浅画三个点 

8.交换缓冲区(可选):

  • 在某些情况下(如双缓冲),需要交换前后缓冲区,以显示渲染的结果。
    // 使用帧缓冲区绘制
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 3);

    // 将结果绘制到屏幕
    gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 绑定默认帧缓冲区
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 3);

纹理

在 WebGL 中,纹理是一种将图像应用于几何体表面的技术。它可以为物体增加细节和真实感。

纹理的基本概念

纹理类型:

  • 2D 纹理:最常用的纹理类型,应用于平面或三维物体表面。
  • 立方体纹理:用于创建环境映射和天空盒等效果,由六个面组成的立方体纹理。
  • 3D 纹理:用于存储体积数据,适用于医学成像等应用。

纹理坐标:

  • 纹理坐标通常用 [0.0, 0.0] 到 [1.0, 1.0] 范围内的值表示,分别对应纹理的左下角和右上角。

纹理过滤:

  • 放大过滤:当纹理比屏幕大时,使用 gl.TEXTURE_MAG_FILTER 设置过滤方式(如 gl.LINEAR、gl.NEAREST)。
  • 缩小过滤:当纹理比屏幕小时,使用 gl.TEXTURE_MIN_FILTER 设置过滤方式。

纹理包裹:

  • 纹理的坐标可以超出 [0.0, 1.0] 范围,使用 gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 设置包裹方式(如 gl.CLAMP_TO_EDGE、gl.REPEAT)。

使用纹理步骤

  1. 创建纹理:使用 gl.createTexture() 创建纹理对象。
  2. 绑定纹理:使用 gl.bindTexture(gl.TEXTURE_2D, texture) 将纹理绑定到当前上下文。
  3. 定义纹理图像:使用 gl.texImage2D() 将图像数据传入纹理。
  4. 设置纹理参数:使用 gl.texParameteri() 设置过滤和包裹方式。
  5. 在着色器中使用纹理:在顶点着色器中传递纹理坐标,并在片元着色器中使用 texture() 函数采样纹理。

直接梭哈:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebGL2 Texture Example</title>
    <style>
        canvas {
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>
<canvas id="glCanvas"></canvas>
<script>
    const canvas = document.getElementById("glCanvas");
    const gl = canvas.getContext("webgl2");

    if (!gl) {
        console.error("无法初始化WebGL2上下文");
    }

    // 创建纹理对象
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // 设定纹理参数
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

    // 创建图像并加载纹理
    const image = new Image();
    image.src = 'a.jpg'; // 替换为你的纹理图像URL
    image.onload = () => {
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        gl.generateMipmap(gl.TEXTURE_2D); // 生成 mipmaps
        render(); // 纹理加载完成后开始渲染
    };

    // 顶点着色器和片元着色器代码
    const vsSource = `
        #version 300 es
        in vec4 aVertexPosition;
        in vec2 aTexCoord;
        out vec2 vTexCoord;
        void main(void) {
            gl_Position = aVertexPosition;
            vTexCoord = aTexCoord;
        }
    `;

    const fsSource = `
        #version 300 es
        precision mediump float;
        in vec2 vTexCoord;
        out vec4 fragColor;
        uniform sampler2D uSampler; // 纹理采样器
        void main(void) {
            fragColor = texture(uSampler, vTexCoord); // 采样纹理
        }
    `;

    // 创建着色器
    function createShader(gl, type, source) {
        const shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            return shader;
        } else {
            console.error(gl.getShaderInfoLog(shader));
            gl.deleteShader(shader);
        }
    }

    // 创建着色器程序
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
    const shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);
    gl.useProgram(shaderProgram);

    // 定义矩形的顶点和纹理坐标
    const vertices = new Float32Array([
        -1.0,  1.0, 0.0, 0.0, // 左上
        -1.0, -1.0, 0.0, 1.0, // 左下
         1.0,  1.0, 1.0, 0.0, // 右上
         1.0, -1.0, 1.0, 1.0  // 右下
    ]);

    // 创建顶点缓冲区
    const vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    // 设置顶点位置和纹理坐标属性
    const positionLocation = gl.getAttribLocation(shaderProgram, "aVertexPosition");
    const texCoordLocation = gl.getAttribLocation(shaderProgram, "aTexCoord");
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 16, 0);
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 16, 8);
    gl.enableVertexAttribArray(texCoordLocation);

    // 主渲染函数
    function render() {
        gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布
        gl.bindTexture(gl.TEXTURE_2D, texture); // 绑定纹理
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); // 绘制矩形
    }
</script>
</body>
</html>

参考代码:

上一篇

react实现编排工具

下一篇

生物信息学简介

©2024 By bajiu.