程序员开发实例大全宝库

网站首页 > 编程文章 正文

webgl系列 - GLSL基础01(webgl opengles)

zazugpt 2024-08-27 23:51:42 编程文章 18 ℃ 0 评论

从本章开始正式进入GLSL的学习,因为后续的代码都是基于glsl开发的,所以得先了解一下这门语言的特性和基础语法,才能看懂后续的代码。

在正式聊GLSL之前,先来看看为什么我们可以编程控制每个像素的显示。这里就要提到渲染管线这个概念。

渲染管线

首先我们在场景中定义的物体最终是怎么显示在屏幕上的呢?

这里我们先来看一张场景渲染流程图

在上述的图例中,一个物体从它自己的本地坐标系,经过模型矩阵,转换到世界坐标系下,在通过观察矩阵转换到观察坐标系下,再通过投影矩阵转换到裁剪坐标系下,最后通过2D屏幕显示在我们的屏幕上,这其中涉及到顶点的变换和屏幕像素的着色,而在这其中我们可以控制的就是顶点渲染管线和片元渲染管线,分别是控制物体的顶点和像素颜色。

着色器的处理是放在GPU中完成的,首先cpu是用来做计算的,他的核心相对较少,不适合高频率大幅度渲染,而显卡一般来讲都容量比CPU高的多的多,而且他拥有强大的并行能力,可以同时处理N个像素分别着色,

我们所提到的渲染管线(Render Pipeline ),就好比一个流水线一样,传入一个顶点,顶点着色器工人计算这个顶点的位置和法线位置等,然后传递给片元着色器工人,由它继续处理给这个顶点构成的面内包含的像素进行上色。然后就能形成我们看到的各种酷炫效果。

着色器初识

目前的着色器主要分为顶点着色器和片元着色器,我们后续的编程也是在这2块程序中进行,那么他们大概长什么样子呢,我们先来看一下

<script id="vertexShader" type="x-shader/x-vertex">
      void main() {
       gl_Position = vec4(position, 1.0);
      }
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
      #ifdef GL_ES
      precision mediump float;
      #endif
      void main(void) {
        gl_FragColor = vec4(0,1,0,1);
      }
</script>

我们在html中一般会使用script标签来存放shader代码,指定script的type为特定类型即可,这样子就不会被js去编译执行,

观察上面的代码,其实他就是一个类C的语言,主入口都在main函数中,在顶点着色器中通过gl_postion这个特定的变量抛出顶点的位置,在片元着色器中通过gl_FragColor这个特定的编程抛出像素的颜色值RGBA。

一个完整例子的代码如下所示:我们在threejs中通过ShaderMaterial来指明现在这个模型的材质由我们自己编写shader代码实现,然后在其中分别传入顶点着色器代码和片元着色器代码即可。

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
    />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <div id="container"></div>
    <script src="https://cdn.bootcss.com/three.js/108/three.min.js"></script>

    <script id="vertexShader" type="x-shader/x-vertex">
      void main() {
       gl_Position = vec4(position, 1.0);
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      #ifdef GL_ES
      precision mediump float;
      #endif
      void main(void) {
        gl_FragColor = vec4(0,1,0,1);
      }
    </script>
    <script>
      // threejs部分
      var container;
      var camera, scene, renderer;
      var uniforms;

      init();
      animate();

      function init() {
        container = document.getElementById("container");

        camera = new THREE.Camera();
        camera.position.z = 1;

        scene = new THREE.Scene();

        var geometry = new THREE.PlaneBufferGeometry(2, 2);

        var material = new THREE.ShaderMaterial({
          vertexShader: document.getElementById("vertexShader").textContent,
          fragmentShader: document.getElementById("fragmentShader").textContent
        });
        material.derivatives = true;

        var mesh = new THREE.Mesh(geometry, material);
        scene.add(mesh);

        renderer = new THREE.WebGLRenderer()
        renderer.setPixelRatio(window.devicePixelRatio);

        container.appendChild(renderer.domElement);

        onWindowResize();
        window.addEventListener("resize", onWindowResize, false);
      }

      function onWindowResize(event) {
        renderer.setSize(window.innerWidth, window.innerHeight);
      }

      function animate() {
        requestAnimationFrame(animate);
        render();
      }

      function render() {
        renderer.render(scene, camera);
      }
    </script>
  </body>
</html>


运行上面的demo你应该能看到一个充满绿色的矩形,你可以试着修改一下gl_fragcolor的值,比如改成下面这样子,你应该能看到一个充满红色的矩形

 gl_FragColor = vec4(1,0,0,1);

这是为什么呢?上面有提到我们gl_FragColor输出的值就是每个像素的颜色值,他的4个分量分别是RGBA的值,这里的值范围都是0-1,而不是传统的0-255,所以1,0,0,1就是表示红色分量最大,绿色和蓝色分量为0的一个颜色值

GLSL数据类型

GLSL和 C 语言程序对应,它包括了以下部分内容:

  • 变量
  • 限定符 attribute、uniform
  • main 函数
  • 结构体
  • 基本赋值语句
  • 内置变量 gl_Position gl_FragCoord等
  • 内置函数(minx / fract等)

1. 变量

变量及变量类型



除上述之外,着色器中还可以将它们构成数组或结构体,以实现更复杂的数据类型。

注意:着色器中是没有string类型的哦

标量

标量对应 C 语言的基础数据类型

float myFloat = 1.0; // 定义一个浮点数
bool myBool = true; // 定义一个布尔值

myFloat = float(myBool); 	// bool -> float
myBool = bool(myFloat);     // float -> bool

向量

当构造向量时,向量构造器中的各参数将会被转换成相同的类型(浮点型、整型或布尔型)。往向量构造器中传递参数有两种形式:

  • 如果向量构造器中只提供了一个标量参数,则向量中所有值都会设定为该标量值。
  • 如果提供了多个标量值或提供了向量参数,则会从左至右使用提供的参数来给向量赋值,如果使用多个标量来赋值,则需要确保标量的个数要多于向量构造器中的个数。

我们在上面提到的gl_position和gl_fragcolor都是。

vec4 myVec4 = vec4(1.0);    // myVec4 = {1.0, 1.0, 1.0, 1.0}
vec3 myVec3 = vec3(1.0, 0.0, 0.5);  // myVec3 = {1.0, 0.0, 0.5}

vec3 temp = vec3(myVec3);    // temp = myVec3
vec2 myVec2 = vec2(myVec3);         // myVec2 = {myVec3.x, myVec3.y}

myVec4 = vec4(myVec2, temp, 0.0);   // myVec4 = {myVec2.x, myVec2.y , temp, 0.0 }

向量中每个分量的获取,可以通过xyzw , rgbastpq关键词来获取到

单独获得向量中的组件有两种方法:即使用 "." 符号或使用数组下标方法。依据构成向量的组件个数,向量的组件可以通过 {x, y, z, w}{r, g, b, a}{s, t, p, q} 等 swizzle 操作来获取。之所以采用这三种不同的命名方法,是因为向量常常会用来表示数学向量、颜色、纹理坐标等。其中的xrs 组件总是表示向量中的第一个元素,如下表:



不同的命名约定是为了方便使用,所以哪怕是描述位置的向量,也是可以通过 {r, g, b, a} 来获取。但是在使用向量时不能混用不同的命名约定,即不能使用 .xgr 这样的方式,每次只能使用同一种命名约定。当使用 "." 操作符时,还可以对向量中的元素重新排序,如下:

vec3 myVec3 = vec3(0.0, 1.0, 2.0); // myVec3 = {0.0, 1.0, 2.0}
vec3 temp;
temp = myVec3.xyz; // temp = {0.0, 1.0, 2.0}
temp = myVec3.xxx; // temp = {0.0, 0.0, 0.0}
temp = myVec3.zyx; // temp = {2.0, 1.0, 0.0}

除了使用 "." 操作符之外,还可以使用数组下标操作。在使用数组下标操作时,元素 [0] 对应的是 x,元素 [1] 对应 y,以此类推。值得注意的是,在 OpenGL ES 2.0 中的某些情况下,数组下标不支持使用非常数的整型表达式(如使用整型变量索引),这是因为对于向量的动态索引操作,某些硬件设备处理起来很困难。在 OpenGL ES 2.0 中仅对 uniform 类型的变量支持这种动态索引。

矩阵

矩阵的构造方法则更加灵活,有以下规则:

  • 如果对矩阵构造器只提供了一个标量参数,该值会作为矩阵的对角线上的值。例如 mat4(1.0) 可以构造一个 4 × 4 的单位矩阵
  • 矩阵可以通过多个向量作为参数来构造,例如一个 mat2 可以通过两个 vec2 来构造
  • 矩阵可以通过多个标量作为参数来构造,矩阵中每个值对应一个标量,按照从左到右的顺序

除此之外,矩阵的构造方法还可以更灵活,只要有足够的组件来初始化矩阵,其构造器参数可以是标量和向量的组合。在 OpenGL ES 中,矩阵的值会以的顺序来存储。在构造矩阵时,构造器参数会按照列的顺序来填充矩阵,如下:

mat3 myMat3 = mat3(1.0, 0.0, 0.0,  // 第一列
                   0.0, 1.0, 0.0,  // 第二列
                   0.0, 1.0, 1.0); // 第三列

矩阵可以认为是向量的组合。例如一个 mat2 可以认为是两个 vec2,一个 mat3 可以认为是三个 vec3 等等。对于矩阵来说,可以通过数组下标 “[]” 来获取某一列的值,然后获取到的向量又可以继续使用向量的操作方法,如下:

mat4 myMat4 = mat4(1.0);  // Initialize diagonal to 1.0 (identity)
vec4 col0 = myMat4[0];     // Get col0 vector out of the matrix 
float m1_1 = myMat4[1][1];  // Get element at [1][1] in matrix 
float m2_2 = myMat4[2].z;   // Get element at [2][2] in matrix

向量和矩阵的操作

绝大多数情况下,向量和矩阵的计算是逐分量进行的(component-wise)。当运算符作用于向量或矩阵时,该运算独立地作用于向量或矩阵的每个分量。 以下是一些示例:

vec3 v, u;
float f;
v = u + f;

等价于:

v.x = u.x + f;
v.y = u.y + f;
v.z = u.z + f;

再如:

vec3 v, u, w;
w = v + u;

等价于:

w.x = v.x + u.x;
w.y = v.y + u.y;
w.z = v.z + u.z;

对于整型和浮点型的向量和矩阵,绝大多数的计算都同上,但是对于向量乘以矩阵、矩阵乘以向量、矩阵乘以矩阵则是不同的计算规则。这三种计算使用线性代数的乘法规则,并且要求参与计算的运算数值有相匹配的尺寸或阶数。 例如:

vec3 v, u;
mat3 m;
u = v * m;

等价于:

u.x = dot(v, m[0]); // m[0] is the left column of m
u.y = dot(v, m[1]); // dot(a,b) is the inner (dot) product of a and b
u.z = dot(v, m[2]);

再如:

u = m * v;

等价于:

u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;

再如:

mat m, n, r;
r = m * n;

等价于:

r[0].x = m[0].x * n[0].x + m[1].x * n[0].y + m[2].x * n[0].z;
r[1].x = m[0].x * n[1].x + m[1].x * n[1].y + m[2].x * n[1].z;
r[2].x = m[0].x * n[2].x + m[1].x * n[2].y + m[2].x * n[2].z;
r[0].y = m[0].y * n[0].x + m[1].y * n[0].y + m[2].y * n[0].z;
r[1].y = m[0].y * n[1].x + m[1].y * n[1].y + m[2].y * n[1].z;
r[2].y = m[0].y * n[2].x + m[1].y * n[2].y + m[2].y * n[2].z;
r[0].z = m[0].z * n[0].x + m[1].z * n[0].y + m[2].z * n[0].z;
r[1].z = m[0].z * n[1].x + m[1].z * n[1].y + m[2].z * n[1].z;
r[2].z = m[0].z * n[2].x + m[1].z * n[2].y + m[2].z * n[2].z;

对于2阶和4阶的向量或矩阵也是相似的规则。

2. 结构体

与 C 语言相似,除了基本的数据类型之外,还可以将多个变量聚合到一个结构体中,下边的示例代码演示了在GLSL中如何声明结构体:

struct customStruct
{
	vec4 color;
	vec2 position;
} customVertex;

首先,定义会产生一个新的类型叫做 customStruct ,及一个名为 customVertex 的变量。结构体可以用构造器来初始化,在定义了新的结构体之后,还会定义一个与结构体类型名称相同的构造器。构造器与结构体中的数据类型必须一一对应,如下:

customVertex = customStruct(
 vec4(0.0, 1.0, 0.0, 0.0), // color
 vec2(0.5, 0.5)   // position
);     

结构体的构造器是基于类型的名称,以参数的形式来赋值。获取结构体内元素的方法和C语言中一致:

vec4 color = customVertex.color;
vec4 position = customVertex.position;

3. 数组

除了结构体外,GLSL 中还支持数组。 语法与 C 语言相似,创建数组的方式如下代码所示:

float floatArray[4];
vec4 vecArray[2];

与C语言不同,在GLSL中,关于数组有两点需要注意:

  • 除了 uniform 变量之外,数组的索引只允许使用常数整型表达式。
  • 在 GLSL 中不能在创建的同时给数组初始化,即数组中的元素需要在定义数组之后逐个初始化,且数组不能使用 const 限定符。

4. 限定符

存储限定符

在声明变量时,应根据需要使用存储限定符来修饰,类似 C 语言中的说明符。GLSL 中支持的存储限定符见下表:

限定符 描述 < none: default > 局部可读写变量,或者函数的参数 const 编译时常量,或只读的函数参数 attribute 由应用程序传输给顶点着色器的逐顶点的数据 uniform 在图元处理过程中其值保持不变,由应用程序传输给着色器 varying 由顶点着色器传输给片段着色器中的插值数据

  • 本地变量和函数参数只能使用 const 限定符,函数返回值和结构体成员不能使用限定符。
  • 数据不能从一个着色器程序传递给下一个阶段的着色器程序,这样会阻止同一个着色器程序在多个顶点或者片段中进行并行计算。
  • 不包含任何限定符或者包含 const 限定符的全局变量可以包含初始化器,这种情况下这些变量会在 main() 函数开始之后第一行代码之前被初始化,这些初始化值必须是常量表达式。
  • 没有任何限定符的全局变量如果没有在定义时初始化或者在程序中被初始化,则其值在进入 main() 函数之后是未定义的。
  • uniform、attribute 和 varying 限定符修饰的变量不能在初始化时被赋值,这些变量的值由 OpenGL ES 计算提供。

参数限定符

GLSL 提供了一种特殊的限定符用来定义某个变量的值是否可以被函数修改,详见下表:

限定符 描述 in 默认使用的缺省限定符,指明参数传递的是值,并且函数不会修改传入的值 inout 指明参数传入的是引用,如果在函数中对参数的值进行了修改,当函数结束后参数的值也会修改 out 参数的值不会传入函数,但是在函数内部修改其值,函数结束后其值会被修改

使用的方式如下边的代码:

vec4 myFunc(inout float myFloat, // inout parameter
            out vec4 myVec4,   // out parameter
            mat4 myMat4);    // in parameter (default)

以下是一个示例函数

float func(in float color2, out float color3){
	color3 =  color2 - 0.1;
	return color3;
}


void main(void) {

  float color3 = 1.0;
  // 传入color3
  func(color2,color3);
  // 不需要再用变量去接收了,它已经被修改了
  gl_FragColor = vec4(color3, 0, 0, 1);
}

精度限定符

OpenGL ES 与 OpenGL 之间的一个区别就是在 GLSL 中引入了精度限定符。精度限定符可使着色器的编写者明确定义着色器变量计算时使用的精度,变量可以选择被声明为低、中或高精度。精度限定符可告知编译器使其在计算时缩小变量潜在的精度变化范围,当使用低精度时,OpenGL ES 的实现可以更快速和低功耗地运行着色器,效率的提高来自于精度的舍弃,如果精度选择不合理,着色器运行的结果会很失真。

OpenGL ES 对各硬件并未强制要求多种精度的支持。其实现可以使用高精度完成所有的计算并且忽略掉精度限定符,然而某些情况下使用低精度的实现会更有优势,精度限定符可以指定整型或浮点型变量的精度,如 lowpmediump,及 highp,如下:

限定符 描述 highp 满足顶点着色语言的最低要求。对片段着色语言是可选项 mediump 满足片段着色语言的最低要求,其对于范围和精度的要求必须不低于lowp并且不高于highp lowp 范围和精度可低于mediump,但仍可以表示所有颜色通道的所有颜色值

具体用法参考以下示例:

highp vec4 position;
varying lowp vec4 color;
mediump float specularExp;

除了精度限定符,还可以指定默认使用的精度。如果某个变量没有使用精度限定符指定使用何种精度,则会使用该变量类型的默认精度。默认精度限定符放在着色器代码起始位置,以下是一些用例:

precision highp float;
precision mediump int;

当为 float 指定默认精度时,所有基于浮点型的变量都会以此作为默认精度,与此类似,为 int 指定默认精度时,所有的基于整型的变量都会以此作为默认精度。在顶点着色器中,如果没有指定默认精度,则 intfloat 都使用 highp,即顶点着色器中,未使用精度限定符指明精度的变量都默认使用最高精度。在片段着色器中,float 并没有默认的精度设置,即片段着色器中必须为 float 默认精度或者为每一个 float 变量指明精度。OpenGL ES 2.0 并未要求其实现在片段着色器中支持高精度,可用是否定义了宏 GL_FRAGMENT_PRECISION_HIGH 来判断是否支持在片段着色器中使用高精度。

在片段着色器中可以使用以下代码:

#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif

这么做可以确保无论实现支持中精度还是高精度都可以完成着色器的编译。注意不同实现中精度的定义及精度的范围都不统一,而是因实现而异的。

ok。今天先写到这里,下一章继续内置变量和内置函数,以及threejs中提供的一些attribute和uniform变量,然后再通过一些demo来夯实这一块的内容。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表