WebGL 之创建 2D 内容
引入 glMatrix 库
该项目使用了 glMatrix 库来执行其矩阵操作,因此需要引入它。本次示例通过 CDN 形式引入使用。
WebGL Demo
着色器
着色器是使用 OpenGL ES 着色语言(GLSL) 编写的程序,它携带着绘制形状的顶点信息以及构造绘制在屏幕上像素的所需数据,换句话说,它负责记录着像素点的位置和颜色。
绘制 WebGL 时候有两种不同的着色器函数,顶点着色器和片段着色器。你需要通过用 GLSL 编写这些着色器,并将代码文本传递给 WebGL,使之在 GPU 执行时编译。顺便一提,顶点着色器和片段着色器的集合我们通常称之为着色器程序。
顶点着色器
每次渲染一个形状时,顶点着色器会在形状中的每个顶点运行。它的工作是将输入顶点从原始坐标系转换到 WebGL 使用的裁剪空间坐标系,其中每个轴的坐标范围从 -1.0 到 1.0,并且不考虑纵横比,实际尺寸或任何其他因素。
顶点着色器需要对顶点坐标进行必要的转换,在每个顶点基础上进行其他调整或计算,然后通过将其保存在由 GLSL 提供的特殊变量(我们称为 gl_Position)中来返回变换后的顶点。
顶点着色器根据需要,也可以完成其他工作。例如,决定哪个包含 texel (en-US) 面部纹理的坐标,可以应用于顶点;通过法线来确定应用到顶点的光照因子等。然后将这些信息存储在变量(varyings)或属性 (attributes)属性中,以便与片段着色器共享。
下面我们通过在 WebGL 环境绘制一个 2D 图像的例子快速介绍这两种着色器。
// Vertex shader program const vsSource = ` attribute vec4 aVertexPosition; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; void main() { gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; } `;
片段着色器
片段着色器在顶点着色器处理完图形的顶点后,会被要绘制的每个图形的每个像素点调用一次。它的职责是确定像素的颜色,通过指定应用到像素的纹理元素(也就是图形纹理中的像素),获取纹理元素的颜色,然后将适当的光照应用于颜色。之后颜色存储在特殊变量 gl_FragColor 中,返回到 WebGL 层。该颜色将最终绘制到屏幕上图形对应像素的对应位置。
const fsSource = ` void main() { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } `;
在学习上述代码前先了解几个关键概念。
- 模型空间(自身变换)
是指一个三维模型在其本地坐标系内的空间。在计算机图形学中,当我们创建一个三维模型时,通常会定义一个本地坐标系来描述该模型的形状和结构。这个本地坐标系就是模型空间。
以下是关于模型空间的一些关键点:
-
本地坐标系:模型空间是相对于模型本身而言的,通常以模型的中心或原点为参考点建立。模型空间的坐标系可以是任意形状和方向,根据设计者的需要来定义。
-
模型位置:在模型空间中,模型的位置、旋转和缩放是相对于本地坐标系进行描述的。这些变换操作可以直接应用于模型空间,而不会影响其他模型或整个场景。
-
局部变换:在模型空间中进行的变换操作通常称为局部变换,因为这些变换只影响到模型本身,而不会影响到其他模型或整个场景。
-
模型编辑:在模型空间中进行编辑和操作是非常直观和方便的,因为所有的变换都是相对于模型本身进行的,可以更容易地调整模型的形状、位置和方向。
-
模型导入:当从外部导入一个三维模型时,该模型通常会以其本地坐标系的形式存在,需要将其转换到世界空间或观察者视图空间才能正确显示在屏幕上。
- 观察者视图空间(控制视角)
是指观察者(通常是相机或眼睛)的视角下的三维空间。在计算机图形学中,观察者视图空间是指将场景中的对象从世界坐标系或模型空间转换到观察者的视角下的坐标系。
以下是观察者视图空间的一些关键概念:
-
相机位置:观察者视图空间以相机位置为原点,相机的视线方向为Z轴方向,通常是一个右手坐标系。这样定义的坐标系使得相机位置处于坐标系的原点,方便描述相机和场景中其他对象之间的相对位置关系。
-
视图变换:将对象从世界坐标系或模型空间转换到观察者视图空间通常需要进行视图变换(View Transformation)。这个变换过程涉及将对象的位置和方向根据相机的位置和朝向进行调整,以便正确呈现在观察者的视野中。
-
相机参数:观察者视图空间的坐标系通常还会考虑相机的参数,如视野角度、近裁剪面和远裁剪面等,以确保只呈现在相机视锥体范围内的对象。
-
投影变换:在观察者视图空间中,还会进行投影变换(Projection Transformation),将三维坐标转换为二维屏幕坐标,以便最终在屏幕上呈现出二维图像。
-
观察者位置:观察者视图空间的坐标系使得观察者位于原点,可以方便地描述场景中的对象相对于观察者的位置和方向。
观察者视图空间(View Space)其实就是指从摄像机或观察者的视角来观察整个场景时的坐标空间。在观察者视图空间中,观察者(通常是摄像机)位于坐标系的原点,观察方向是坐标系的负Z轴方向,摄像机的朝向和位置决定了观察者视图空间的坐标系。
通俗来说,观察者视图空间就好比是我们通过摄像机所看到的整个世界的视角。摄像机的位置和朝向决定了我们观察到的场景是什么样子,就像我们在现实生活中移动自己的眼睛或头部来改变视角一样。观察者视图空间让我们能够从不同的角度观察和渲染3D场景,帮助我们更好地理解和呈现复杂的三维图形。
- 裁剪空间(未显示在屏幕的元素隐藏)
是计算机图形学中的一个重要概念,它是在顶点着色器处理后的一种特殊坐标空间。裁剪空间是顶点着色器输出的最后一个坐标空间,接下来会通过投影变换将其转换为标准化设备坐标(Normalized Device Coordinates,NDC),最终在屏幕上渲染出图像。
以下是裁剪空间的详细解释:
-
生成裁剪空间:在顶点着色器中,顶点数据经过模型变换、视图变换和投影变换后,得到的坐标被称为裁剪空间坐标。这些坐标通常是四维的齐次坐标(homogeneous coordinates),其中X、Y、Z坐标表示物体的位置,而W坐标用于透视矫正。
-
裁剪空间坐标系:裁剪空间坐标系的范围是在X、Y、Z坐标轴上都在-1到1之间,W坐标通常不做限制。这个范围内的坐标表示的是相对于视锥体(View Frustum)的位置,超出这个范围的坐标将被裁剪掉,不会显示在最终的图像中。
-
裁剪空间坐标的作用:裁剪空间坐标的主要作用是进行裁剪操作,即将超出视锥体范围的坐标进行裁剪,确保只有位于视锥体内的物体才会被显示出来。
-
投影变换:裁剪空间坐标通过投影变换(Projection Transformation)将其转换为标准化设备坐标(NDC),这是一个将裁剪空间坐标映射到屏幕坐标的过程,最终在屏幕上呈现出图像。
-
裁剪空间与屏幕空间:裁剪空间坐标经过投影变换后得到的标准化设备坐标是一个三维坐标,其中X和Y坐标范围在-1到1之间,Z坐标范围在0到1之间。这些标准化设备坐标最终会被映射到屏幕空间,通过视口变换(Viewport Transformation)转换为屏幕上的二维像素坐标。
让我再来解释下裁剪空间,就是当我们在网页上展示一个3D场景时,比如一个旋转的立方体,计算机需要知道哪些部分应该显示在屏幕上,哪些部分应该被隐藏起来。裁剪空间就像是一个虚拟的箱子,只有在这个箱子里的东西才会被展示在屏幕上,超出这个箱子的部分会被“剪掉”,不会被显示出来。
所以,裁剪空间就是一个用来控制显示内容范围的虚拟空间,确保只有在这个空间内的东西才会被最终展示在屏幕上,其他超出范围的部分会被裁剪掉,让我们看到的画面更加清晰和合理。
让我们再来看看这些变量的作用:
uModelViewMatrix(模型空间转换到观察者视图空间)
是一个在顶点着色器中使用的 uniform 变量,用于表示模型视图矩阵。在图形渲染中,模型视图矩阵通常用于将顶点从模型空间转换到观察者视图空间。
以下是关于 uModelViewMatrix 的详细解释:
-
uniform 变量:在着色器中,uniform 变量是一种全局变量,其数值在每次渲染调用时保持不变。在 JavaScript 程序中设置 uniform 变量的值,然后传递给着色器使用。
-
mat4:mat4 是 WebGL 中表示 4x4 矩阵的数据类型。在这里,uModelViewMatrix 是一个 4x4 的矩阵,用于进行模型视图变换操作。
-
模型视图矩阵:模型视图矩阵用于将顶点从模型空间转换到观察者视图空间。模型视图矩阵通常包含了模型的位置、旋转和缩放信息,以及观察者的位置和方向信息。
-
作用:在顶点着色器中,uModelViewMatrix 通常与顶点位置属性一起使用,通过乘法操作将顶点从模型空间变换到观察者视图空间。这个变换过程是基本的几何变换,用于确定物体在观察者眼中的位置和方向。
-
使用:在 JavaScript 程序中,可以通过设置 uModelViewMatrix 的值来控制模型的位置、旋转和缩放,从而影响最终的渲染效果。在顶点着色器中,将顶点位置乘以 uModelViewMatrix 可以实现模型的位置和姿态变换。
通过使用 uModelViewMatrix 这样的 uniform 变量,可以在顶点着色器中实现各种模型的位置和视图变换,从而实现复杂的三维图形渲染效果。
uProjectionMatrix(三维场景投影到二维屏幕上)
表示投影矩阵(Projection Matrix)中的 uniform 变量。投影矩阵在计算机图形学中扮演着至关重要的角色,用于将三维场景投影到二维屏幕上,实现透视效果。下面是对uProjectionMatrix的详细解释:
-
投影矩阵:
- 投影矩阵是一种特殊的矩阵,用于将三维场景中的物体坐标转换为二维屏幕坐标,同时考虑到透视效果。投影矩阵通常包括正交投影(Orthographic Projection)和透视投影(Perspective Projection)两种类型。
-
uProjectionMatrix:
- 在着色器程序中,uProjectionMatrix通常是一个uniform变量,用于传递投影矩阵给顶点着色器或片段着色器。uniform变量是在渲染过程中在CPU端设置的变量,可以在所有顶点或片段之间共享。
-
投影矩阵的作用:
- 投影矩阵的作用是将裁剪空间坐标(Clip Space)转换为标准化设备坐标(NDC),并最终将其映射到屏幕空间。投影矩阵考虑了视锥体的透视效果,确保远处的物体看起来较小。
-
构建投影矩阵:
- 构建投影矩阵的方法取决于所需的投影类型。对于透视投影,可以使用类似透视投影矩阵的公式进行构建,考虑视角、纵横比等因素。对于正交投影,构建方法更为简单,通常是一个等距投影的矩阵。
-
传递uProjectionMatrix:
- 在渲染过程中,通常会在CPU端计算投影矩阵,并将其传递给GPU端的着色器程序。在顶点着色器中,uProjectionMatrix会被用来将顶点从裁剪空间转换到标准化设备坐标。
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition这个公式是一个在OpenGL或类似的图形渲染API中常见的顶点着色器(Vertex Shader)中的计算过程,用于将顶点从模型空间转换到裁剪空间。让我们逐步解释这个公式:
-
aVertexPosition:这是顶点着色器中一个输入变量,代表顶点在模型空间中的位置。通常,这个变量包含了顶点的坐标信息(通常是三维坐标)。
-
uModelViewMatrix:这是一个统一变量(Uniform),代表模型视图矩阵(Model-View Matrix)。模型视图矩阵用于将顶点从模型空间变换到观察者视图空间。这个矩阵包含了模型空间到世界空间和观察者视图空间的变换信息。
-
uProjectionMatrix:同样是一个统一变量,代表投影矩阵(Projection Matrix)。投影矩阵用于将顶点从观察者视图空间变换到裁剪空间,实现透视效果和裁剪操作。
-
gl_Position:这是顶点着色器中的内建变量,用于存储最终计算出的顶点在裁剪空间中的位置。顶点着色器的最终目标是计算出每个顶点在裁剪空间中的位置,以便后续的裁剪、透视除法和屏幕映射等操作。
因此,公式 gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition 的含义是:首先将顶点从模型空间通过模型视图矩阵变换到观察者视图空间,然后再通过投影矩阵将顶点变换到裁剪空间,最终得到顶点在裁剪空间中的位置,并存储在 gl_Position 中,以便后续的图形渲染操作。这个过程是图形渲染中非常关键的一步,用于控制顶点在屏幕上的最终呈现效果。
初始化着色器
现在我们已经定义了两个着色器,我们需要将它们传递给 WebGL,编译并将它们连接在一起。下面的代码通过调用 loadShader(),为着色器传递类型和来源,创建了两个着色器。然后创建一个附加着色器的程序,将它们连接在一起。如果编译或链接失败,代码将弹出 alert。
// webgl-demo.js // 初始化着色器程序,让 WebGL 知道如何绘制我们的数据 function initShaderProgram(gl, vsSource, fsSource) { const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); // 创建着色器程序 const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // 如果创建失败,alert if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert( "Unable to initialize the shader program: " + gl.getProgramInfoLog(shaderProgram), ); return null; } return shaderProgram; } // 创建指定类型的着色器,上传 source 源码并编译 function loadShader(gl, type, source) { const shader = gl.createShader(type); // Send the source to the shader object gl.shaderSource(shader, source); // Compile the shader program gl.compileShader(shader); // See if it compiled successfully if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert( "An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader), ); gl.deleteShader(shader); return null; } return shader; }
这段代码包含了两个函数:initShaderProgram(gl, vsSource, fsSource) 和 loadShader(gl, type, source),用于初始化着色器程序和创建着色器对象。
-
loadShader(gl, type, source) 函数:
- 这个函数用于创建指定类型(顶点着色器或片元着色器)的着色器对象,并将源码传递给着色器对象进行编译。
- gl.createShader(type):创建一个指定类型的着色器对象。
- gl.shaderSource(shader, source):将着色器源码传递给着色器对象。
- gl.compileShader(shader):编译着色器程序。
- gl.getShaderParameter(shader, gl.COMPILE_STATUS):检查着色器编译是否成功。
- 如果编译失败,会使用alert弹出错误信息,并删除着色器对象,最后返回null。
- 如果编译成功,返回编译后的着色器对象。
-
initShaderProgram(gl, vsSource, fsSource) 函数:
- 这个函数用于初始化着色器程序,包括创建顶点着色器和片元着色器,然后将它们链接到一个着色器程序中。
- 首先调用loadShader函数分别加载顶点着色器和片元着色器。
- 使用gl.createProgram()创建一个着色器程序对象。
- 使用gl.attachShader将顶点着色器和片元着色器附加到着色器程序对象上。
- 调用gl.linkProgram(shaderProgram)链接着色器程序。
- 检查链接状态,如果链接失败,使用alert弹出错误信息,并返回null。
- 如果链接成功,返回着色器程序对象。
总体来说,这两个函数的作用是帮助创建和初始化顶点着色器和片元着色器,并将它们链接到一个着色器程序中。这是在WebGL中使用着色器的常见操作步骤,用于准备着色器程序以在绘制过程中使用。
gl.linkProgram(shaderProgram) 是 WebGL 中用于链接着色器程序的方法。当调用这个方法时,它会执行以下操作:
-
将顶点着色器和片元着色器链接到着色器程序:
- gl.attachShader(shaderProgram, vertexShader) 和 gl.attachShader(shaderProgram, fragmentShader) 用于将顶点着色器和片元着色器附加到着色器程序对象上。
-
执行链接操作:
- gl.linkProgram(shaderProgram) 方法实际上执行着色器程序的链接操作。这个操作会将附加到着色器程序的顶点着色器和片元着色器链接在一起,以创建一个完整的着色器程序。
-
链接状态检查:
- 连接完成后,可以使用 gl.getProgramParameter(shaderProgram, gl.LINK_STATUS) 来检查链接的状态。如果链接成功,gl.getProgramParameter 返回 true,否则返回 false。
-
错误信息获取:
- 如果链接失败,可以通过调用 gl.getProgramInfoLog(shaderProgram) 获取链接失败的具体信息,例如链接错误的原因、问题所在等。
-
返回值:
- 如果链接成功,该函数会返回链接后的着色器程序对象,可以在后续绘制操作中使用。
- 如果链接失败,通常会进行错误处理并返回 null 或者其他适当的数值。
总的来说,gl.linkProgram(shaderProgram) 是将顶点着色器和片元着色器链接到一起,形成一个可用于绘制的着色器程序的关键步骤。通过这一步,WebGL 将顶点着色器和片元着色器组合在一起,为后续的绘制操作做好准备。
创建对象
在画正方形前,我们需要创建一个缓冲器来存储它的顶点。我们会用到名字为 initBuffers() 的函数。当我们了解到更多 WebGL 的高级概念时,这段代码会将有更多参数,变得更加复杂,并且用来创建更多的三维物体。
// init-buffers.js function initBuffers(gl) { // 函数初始化顶点位置缓冲区,并将结果存储在 positionBuffer 中 const positionBuffer = initPositionBuffer(gl); return { position: positionBuffer, }; } function initPositionBuffer(gl) { // 创建一个新的缓冲区对象,用于存储顶点数据 const positionBuffer = gl.createBuffer(); // 将新创建的缓冲区绑定到 WebGL 的 ARRAY_BUFFER 目标上,表示接下来的操作将应用于这个缓冲区 gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // 定义了一个包含顶点位置数据的 JavaScript 数组 positions,这里是一个简单的正方形的顶点坐标 const positions = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0]; // 将顶点位置数据传递给 WebGL,创建一个 Float32Array 类型的数据缓冲区,然后将数据存储到当前绑定的缓冲区中 gl.STATIC_DRAW表示这些数据不会经常改变 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); // 返回创建的顶点位置缓冲区对象 positionBuffer return positionBuffer; } export { initBuffers };
绘制场景
当着色器和物体都创建好后,我们可以开始渲染这个场景了。因为我们这个例子不会产生动画,所以 drawScene() 方法非常简单。它还使用了几个工具函数,稍后我们会介绍。
// draw-scene.js // 这个函数用于绘制场景,包括设置清除颜色、深度测试、透视投影矩阵、模型视图矩阵、顶点属性等 function drawScene(gl, programInfo, buffers) { gl.clearColor(0.0, 0.0, 0.0, 1.0); // 清除颜色设置为黑色,完全不透明 gl.clearDepth(1.0); // 清除深度缓冲区 gl.enable(gl.DEPTH_TEST); // 启用深度测试 gl.depthFunc(gl.LEQUAL); // 设置深度测试函数为LEQUAL,近处物体遮挡远处物体 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 清除颜色缓冲区和深度缓冲区 const fieldOfView = (45 * Math.PI) / 180; // 透视视角为45度,转换为弧度 const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; // 计算画布宽高比 const zNear = 0.1; // 相机到近裁剪面的距离 const zFar = 100.0; // 相机到远裁剪面的距离 const projectionMatrix = mat4.create(); // 创建透视投影矩阵 mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar); // 设置透视投影矩阵 const modelViewMatrix = mat4.create(); // 创建模型视图矩阵 mat4.translate( modelViewMatrix, // 目标矩阵 modelViewMatrix, // 要进行平移的矩阵 [-0.0, 0.0, -6.0] // 平移量 ); // 平移模型视图矩阵到指定位置 { const numComponents = 2; // 每次迭代提取2个值 const type = gl.FLOAT; // 缓冲区中的数据是32位浮点数 const normalize = false; // 不归一化 const stride = 0; // 从一个值组到下一个值组的字节数 const offset = 0; // 缓冲区内开始提取的字节数 gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position); // 绑定位置缓冲区 gl.vertexAttribPointer( programInfo.attribLocations.vertexPosition, // 顶点位置属性的位置 numComponents, // 每个顶点属性的分量数 type, // 数据类型 normalize, // 是否归一化 stride, // 步长 offset // 偏移量 ); gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); // 启用顶点位置属性 } gl.useProgram(programInfo.program); // 使用着色器程序 gl.uniformMatrix4fv( programInfo.uniformLocations.projectionMatrix, // 投影矩阵的位置 false, // 是否转置 projectionMatrix // 投影矩阵的值 ); gl.uniformMatrix4fv( programInfo.uniformLocations.modelViewMatrix, // 模型视图矩阵的位置 false, // 是否转置 modelViewMatrix // 模型视图矩阵的值 ); { const offset = 0; // 偏移量 const vertexCount = 4; // 顶点数量 gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount); // 绘制三角带 } } // 告诉 WebGL 如何从顶点缓冲区中提取顶点位置数据并传递给顶点着色器 function setPositionAttribute(gl, buffers, programInfo) { const numComponents = 2; // 每次迭代提取2个值 const type = gl.FLOAT; // 缓冲区中的数据是32位浮点数 const normalize = false; // 不归一化 const stride = 0; // 从一个值组到下一个值组的字节数 const offset = 0; // 缓冲区内开始提取的字节数 gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position); // 绑定位置缓冲区 gl.vertexAttribPointer( programInfo.attribLocations.vertexPosition, // 顶点位置属性的位置 numComponents, // 每个顶点属性的分量数 type, // 数据类型 normalize, // 是否归一化 stride, // 步长 offset // 偏移量 ); gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); // 启用顶点位置属性 } export { drawScene };
第一步,用背景色擦除画布,接着建立摄像机透视矩阵。设置 45 度的视图角度,并且设置一个适合实际图像的宽高比。指定在摄像机距离 0.1 到 100 单位长度的范围内的物体可见。
接着加载特定位置,并把正方形放在距离摄像机 6 个单位的位置。然后,我们绑定正方形的顶点缓冲到上下文,并配置好,再通过调用 drawArrays() 方法来画出对象。
最后,让我们引入 initBuffers() 和 drawScene()。
在“webgl-demo.js”文件头部添加如下代码:
import { initBuffers } from "./init-buffers.js"; import { drawScene } from "./draw-scene.js";
// main()函数 const buffers = initBuffers(gl); // Draw the scene drawScene(gl, programInfo, buffers);
完整源码
如下:
WebGL Demo
// webgl-demo.js import { initBuffers } from "./init-buffers.js"; import { drawScene } from "./draw-scene.js"; main(); // // start here // function main() { const canvas = document.querySelector("#glcanvas"); // Initialize the GL context const gl = canvas.getContext("webgl"); // Only continue if WebGL is available and working if (gl === null) { alert( "Unable to initialize WebGL. Your browser or machine may not support it." ); return; } // Set clear color to black, fully opaque gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear the color buffer with specified clear color gl.clear(gl.COLOR_BUFFER_BIT); // Vertex shader program const vsSource = ` attribute vec4 aVertexPosition; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; void main() { gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; } `; const fsSource = ` void main() { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } `; // Initialize a shader program; this is where all the lighting // for the vertices and so forth is established. const shaderProgram = initShaderProgram(gl, vsSource, fsSource); // Collect all the info needed to use the shader program. // Look up which attribute our shader program is using // for aVertexPosition and look up uniform locations. const programInfo = { program: shaderProgram, attribLocations: { vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"), }, uniformLocations: { projectionMatrix: gl.getUniformLocation( shaderProgram, "uProjectionMatrix" ), modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"), }, }; // Here's where we call the routine that builds all the // objects we'll be drawing. const buffers = initBuffers(gl); // Draw the scene drawScene(gl, programInfo, buffers); } // // Initialize a shader program, so WebGL knows how to draw our data // function initShaderProgram(gl, vsSource, fsSource) { const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); // Create the shader program const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // If creating the shader program failed, alert if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert( `Unable to initialize the shader program: ${gl.getProgramInfoLog( shaderProgram )}` ); return null; } return shaderProgram; } // // creates a shader of the given type, uploads the source and // compiles it. // function loadShader(gl, type, source) { const shader = gl.createShader(type); // Send the source to the shader object gl.shaderSource(shader, source); // Compile the shader program gl.compileShader(shader); // See if it compiled successfully if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert( `An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}` ); gl.deleteShader(shader); return null; } return shader; }
// init-buffers.js function initBuffers(gl) { const positionBuffer = initPositionBuffer(gl); return { position: positionBuffer, }; } function initPositionBuffer(gl) { // Create a buffer for the square's positions. const positionBuffer = gl.createBuffer(); // Select the positionBuffer as the one to apply buffer // operations to from here out. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // Now create an array of positions for the square. const positions = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0]; // Now pass the list of positions into WebGL to build the // shape. We do this by creating a Float32Array from the // JavaScript array, then use it to fill the current buffer. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); return positionBuffer; } export { initBuffers };
// draw-scene.js function drawScene(gl, programInfo, buffers) { gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque gl.clearDepth(1.0); // Clear everything gl.enable(gl.DEPTH_TEST); // Enable depth testing gl.depthFunc(gl.LEQUAL); // Near things obscure far things // Clear the canvas before we start drawing on it. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Create a perspective matrix, a special matrix that is // used to simulate the distortion of perspective in a camera. // Our field of view is 45 degrees, with a width/height // ratio that matches the display size of the canvas // and we only want to see objects between 0.1 units // and 100 units away from the camera. const fieldOfView = (45 * Math.PI) / 180; // in radians const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; const zNear = 0.1; const zFar = 100.0; const projectionMatrix = mat4.create(); // note: glmatrix.js always has the first argument // as the destination to receive the result. mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar); // Set the drawing position to the "identity" point, which is // the center of the scene. const modelViewMatrix = mat4.create(); // Now move the drawing position a bit to where we want to // start drawing the square. mat4.translate( modelViewMatrix, // destination matrix modelViewMatrix, // matrix to translate [-0.0, 0.0, -6.0] ); // amount to translate // Tell WebGL how to pull out the positions from the position // buffer into the vertexPosition attribute. setPositionAttribute(gl, buffers, programInfo); // Tell WebGL to use our program when drawing gl.useProgram(programInfo.program); // Set the shader uniforms gl.uniformMatrix4fv( programInfo.uniformLocations.projectionMatrix, false, projectionMatrix ); gl.uniformMatrix4fv( programInfo.uniformLocations.modelViewMatrix, false, modelViewMatrix ); { const offset = 0; const vertexCount = 4; gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount); } } // Tell WebGL how to pull out the positions from the position // buffer into the vertexPosition attribute. function setPositionAttribute(gl, buffers, programInfo) { const numComponents = 2; // pull out 2 values per iteration const type = gl.FLOAT; // the data in the buffer is 32bit floats const normalize = false; // don't normalize const stride = 0; // how many bytes to get from one set of values to the next // 0 = use type and numComponents above const offset = 0; // how many bytes inside the buffer to start from gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position); gl.vertexAttribPointer( programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset ); gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); } export { drawScene };
-
-