WebGL 从0到1绘制一个立方体

目录

前言 

组成立方体的面、三角形、顶点坐标和顶点颜色

通过顶点索引绘制物体

gl.drawElements(mode, count, type, offset) 函数规范

示例程序 彩色立方体(HelloCube.js)

代码详解 

向缓冲区中写入顶点的坐标、颜色与索引 

 gl.ELEMENT_ARRAY_BUFFER 绑定顶点索引数据目标

 gl.drawElements()详解

示例效果

为立方体的每个表面指定颜色

组成立方体的面、三角形和顶点的关系(为每个面指定不同的颜色)

示例程序 6面6颜色立方体(ColoredCube.js)


前言 

我们来绘制如下图所示的立方体(图右侧显示了立方体每个顶点的坐标),其8个顶点的颜色分别为白色、品红色(亮紫色)、红色、黄色、绿色、青色(蓝绿色)、蓝色、黑色。你也许知道,为每个顶点定义颜色后,表面上的颜色会根据顶点颜色内插出来,形成一种光滑的渐变效果(“色体”,相当于二维的“色轮”)。

绘制三角形,我们都是调用gl.drawArrays()方法来进行绘制操作的。考虑一下,如何用该函数绘制出一个立方体呢。我们只能使用gl.TRIANGLES、gl.TRIANGLE_STRIP或者gl.TRIANGLE_FAN模式来绘制三角形,那么最简单也最直接的方法就是,通过绘制两个三角形来拼成立方体的一个矩形表面。换句话说,为了绘制四个顶点(v0,v1,v2,v3)组成的矩形表面,你可以分别绘制三角形(v0,v1,v2)和三角形(v0,v2,v3)。对立方体的所有表面都这样做就绘制出了整个立方体。在这种情况下,缓冲区内的顶点坐标应该是这样的: 

          

立方体的每一个面由两个三角形组成,每个三角形有3个顶点,所以每个面需要用到6个顶点。立方体共有6个面,一共需要6×6=36个顶点。将36个顶点的数据写入缓冲区,再调用gl.drawArrays(gl.TRIANGLES,0,36)就可以绘制出立方体。问题是,立方体实际只有8个顶点,而我们却定义了36个之多,这是因为每个顶点都会被多个三角形共用。

或者,你也可以使用gl.TRIANGLE_FAN模式来绘制立方体。在gl.TRIANGLE_FAN模式下,用4个顶点(v0,v1,v2,v3)就可以绘制出一个四边形,所以你只需要4×6=24个顶点。但是,如果这样做你就必须为立方体的每个面调用一次gl.drawArrays(),一共需要6次调用。所以,两种绘制模式各有优缺点,没有一种是完美的。

如你所愿,WebGL确实提供了一种完美的方案:gl.drawElements()。使用该函数替代gl.drawArrays()函数进行绘制,能够避免重复定义顶点,保持顶点数量最小。为此,你需要知道模型的每一个顶点的坐标,这些顶点坐标描述了整个模型(立方体)。

组成立方体的面、三角形、顶点坐标和顶点颜色

我们将立方体拆成顶点和三角形,如下图左)所示。立方体被拆成6个面:前、后、左、右、上、下,每个面都由两个三角形组成,与三角形列表中的两个三角形相关联。每个三角形都有3个顶点,与顶点列表中的3个顶点相关联,如下图(右)所示。三角形列表中的数字表示该三角形的3个顶点在顶点列表中的索引值。顶点列表中共有8个顶点,索引值为从0到7。

 这样用一个数据结构就可以描述出立方体是怎样由顶点坐标和颜色构成的了。

通过顶点索引绘制物体

到目前为止,我们都是使用gl.drawArrays()进行绘制,现在我们要使用另一个方法gl.drawElements()。两个方法看上去差不多,但后者有一些优势,我们稍后再解释。首先,我们来看一下如何使用gl.drawElements()。我们需要在gl.ELEMENT_ARRAY_BUFFER(而不是之前一直使用的gl.ARRAY_BUFFER)中指定顶点的索引值。所以两种方法最重要的区别就在于gl.ELEMENT_ARRAY_BUFFER,它管理着具有索引结构的三维模型数据。

gl.drawElements(mode, count, type, offset) 函数规范

我们需要将顶点索引(也就是三角形列表中的内容)写入到缓冲区中,并绑定到gl.ELEMENT_ARRAY_BUFFER上,其过程类似于调用gl.drawArrays()时将顶点坐标写入缓冲区并将其绑定到gl.ARRAY_BUFFER上的过程。也就是说,可以继续使用gl.bindBuffer()和gl.bufferData()来进行上述操作,只不过参数target要改为gl.ELEMENT_ARRAY_BUFFER。来看一下示例程序。 

示例程序 彩色立方体(HelloCube.js)

如下显示了程序的代码。本例使用了金字塔状的可视空间和透视投影变换,顶点着色器对顶点坐标进行了简单的变换,片元着色器接收varying变量并赋值给gl_FragColor,以对片元进行着色。使用gl.drawElements()或是gl.drawArrays()对上述这些内容没有影响,真正影响到的内容在initVertexBuffers()函数中。

var VSHADER_SOURCE ='attribute vec4 a_Position;\n' +'attribute vec4 a_Color;\n' +'uniform mat4 u_MvpMatrix;\n' +'varying vec4 v_Color;\n' +'void main() {\n' +'  gl_Position = u_MvpMatrix * a_Position;\n' +'  v_Color = a_Color;\n' +'}\n';var FSHADER_SOURCE ='#ifdef GL_ES\n' +'precision mediump float;\n' +'#endif\n' +'varying vec4 v_Color;\n' +'void main() {\n' +'  gl_FragColor = v_Color;\n' +'}\n';function main() {var canvas = document.getElementById('webgl');var gl = getWebGLContext(canvas);if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) return// 设置顶点坐标和颜色var n = initVertexBuffers(gl);gl.clearColor(0.0, 0.0, 0.0, 1.0); // 设置清除背景色gl.enable(gl.DEPTH_TEST); // 开启隐藏面消除var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');var mvpMatrix = new Matrix4();mvpMatrix.setPerspective(30, 1, 1, 100); // 设置投影矩阵mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0); // 设置视图矩阵gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements); // 将模型视图矩阵传给u_MvpMatrixgl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 清空颜色缓冲区和深度缓冲区// 绘制立方体gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}function initVertexBuffers(gl) {// Create a cube//    v6----- v5//   /|      /|//  v1------v0|//  | |     | |//  | |v7---|-|v4//  |/      |///  v2------v3var verticesColors = new Float32Array([// 顶点坐标和颜色(顶点坐标分别对应顶点索引0~7)1.0,  1.0,  1.0,     1.0,  1.0,  1.0,  // v0 White-1.0,  1.0,  1.0,     1.0,  0.0,  1.0,  // v1 Magenta-1.0, -1.0,  1.0,     1.0,  0.0,  0.0,  // v2 Red1.0, -1.0,  1.0,     1.0,  1.0,  0.0,  // v3 Yellow1.0, -1.0, -1.0,     0.0,  1.0,  0.0,  // v4 Green1.0,  1.0, -1.0,     0.0,  1.0,  1.0,  // v5 Cyan-1.0,  1.0, -1.0,     0.0,  0.0,  1.0,  // v6 Blue-1.0, -1.0, -1.0,     0.0,  0.0,  0.0   // v7 Black]);// 顶点索引var indices = new Uint8Array([0, 1, 2,   0, 2, 3,    // front0, 3, 4,   0, 4, 5,    // right0, 5, 6,   0, 6, 1,    // up1, 6, 7,   1, 7, 2,    // left7, 4, 3,   7, 3, 2,    // down4, 7, 6,   4, 6, 5     // back]);// 创建缓冲区对象var vertexColorBuffer = gl.createBuffer();// 创建用于管理顶点索引数据的缓冲区对象var indexBuffer = gl.createBuffer();// 将顶点坐标和颜色写入缓冲区对象gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);var FSIZE = verticesColors.BYTES_PER_ELEMENT;// 将顶点坐标和颜色写入缓冲区对象var a_Position = gl.getAttribLocation(gl.program, 'a_Position');gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);gl.enableVertexAttribArray(a_Position);var a_Color = gl.getAttribLocation(gl.program, 'a_Color');gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);gl.enableVertexAttribArray(a_Color);// 将顶点索引数据写入缓冲区对象gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);return indices.length;
}

代码详解 

main()函数的流程,我们首先调用initVertexBuffers()函数将顶点数据写入缓冲区(第25行),然后开启隐藏面消除(第27行),使WebGL能够根据立方体各表面的前后关系正确地进行绘制。

接着,设置视点和可视空间(第29~31行),把模型视图投影矩阵传给顶点着色器中的u_MvpMatrix变量。

最后,清空颜色和深度缓冲区(第33行),使用gl.drawElements()绘制立方体(第36行)。该函数的使用方法,下面具体来看一下。 

向缓冲区中写入顶点的坐标、颜色与索引 

本例的initVertexBuffers()函数通过缓冲区对象verticesColors向顶点着色器中的attribute变量传顶点坐标和颜色信息,这一点与之前无异。但是,本例不再按照verticesColors中的顶点顺序来进行绘制,所以必须额外注意每个顶点的索引值,我们要通过索引值来指定绘制的顺序。比如说,第1个顶点的索引为0,第2个顶点的索引为1,等等。下面是initVertexBuffers()函数的部分代码:

 gl.ELEMENT_ARRAY_BUFFER 绑定顶点索引数据目标

也许你会注意到,缓冲区对象indexBuffer(第76行)中的数据来自于数组indices(第61行),该数组以索引值的形式存储了绘制顶点的顺序。索引值是整型数,所以数组的类型是Uint8Array(无符号8位整型数)。如果有超过256个顶点,那么就应该使用Uint16Array。indices中的元素如下图中的三角形列表所示,每3个索引值为1组,指向3个顶点,由这3个顶点组成1个三角形。通常我们不需要手动创建这些顶点和索引数据,因为三维建模工具会帮助我们创建它们。 

绑定缓冲区,以及向缓冲区写入索引数据的过程(第89~90行)与之前示例程序中的很类似,区别就是绑定的目标由gl.ARRAY_BUFFER变成了gl.ELEMENT_ARRAY_BUFFER。这个参数告诉WebGL,该缓冲区中的内容是顶点的索引值数据。 

此时,WebGL系统的内部状态如下图所示。

最后,我们调用gl.drawElements(),就绘制出了立方体(第36行)。 

 gl.drawElements()详解

gl.drawElements()方法的第2个参数n表示顶点索引数组的长度,也就是顶点着色器的执行次数。注意,n与gl.ARRAY_BUFFER中的顶点个数不同。 

在调用gl.drawElements()时,WebGL首先从绑定到gl.ELEMENT_ARRAY_BUFFER的缓冲区(也就是indexBuffer)中获取顶点的索引值,然后根据该索引值,从绑定到gl.ARRAY_BUFFER的缓冲区(即vertexColorBuffer)中获取顶点的坐标、颜色等信息,然后传递给attribute变量并执行顶点着色器。对每个索引值都这样做,最后就绘制出了整个立方体,而此时你只调用了一次gl.drawElements()。这种方式通过索引来访问顶点数据,从而循环利用顶点信息,控制内存的开销,但代价是你需要通过索引来间接地访问顶点,在某种程度上使程序复杂化了。所以,gl.drawElements()和gl.drawArrays()各有优劣,具体用哪一个取决于具体的系统需求。

 虽然我们已经证明了gl.drawElements()是高效的绘制三维图形的方式,但还是漏了关键的一点:我们无法通过将颜色定义在索引值上,颜色仍然是依赖于顶点的,如下图。

示例效果

考虑这样的情况:我们希望立方体的每个表面都是不同的单一颜色(而非颜色渐变效果)或者纹理图像,如下图所示。我们需要把每个面的颜色或纹理信息写入三角形列表、索引和顶点数据中

下面,我们将研究如何解决这个问题,以及如何为每个面指定颜色

为立方体的每个表面指定颜色

我们知道,顶点着色器进行的是逐顶点的计算,接收的是逐顶点的信息。这说明,如果你想指定表面的颜色,你也需要将颜色定义为逐顶点的信息,并传给顶点着色器。举个例子,你想把立方体的前表面涂成蓝色,前表面由顶点v0、v1、v2、v3组成,那么你就需要将这4个顶点都指定为蓝色。

组成立方体的面、三角形和顶点的关系(为每个面指定不同的颜色)

但是你会发现,顶点v0不仅在前表面上,也在右表面和上表面上,如果你将v0指定为蓝色,那么它在另外两个表面上也会是蓝色,这不是我们想要的结果。为了解决这个问题,我们需要创建多个具有相同顶点坐标的顶点(虽然这样会造成一些冗余),如下图所示。如果这样做,你就必须把那些具有相同坐标的顶点分开处理

此时的三角形列表,也就是顶点索引值序列,对每个面都指向一组不同的顶点,不再有前表面和上表面共享一个顶点的情况。这样一来,就可以实现前述的效果,为每个表面涂上不同的单色了。我们也可以使用类似的方法为立方体的每个表面贴上不同的纹理,只需要将上图中的颜色值换成纹理坐标即可。 

现在来看一下示例程序ColoredCube的代码,它绘制出了一个立方体,其每个表面涂上了不同的颜色。

示例程序 6面6颜色立方体(ColoredCube.js)

示例程序代码如下所示。本例ColoredCube.js与HelloCube.js的主要区别是在于顶点数据存储在缓冲区中的形式,也就是initVertexBuffers()函数负责的内容。两者的主要区别是:

● 在HelloCube.js中,顶点的坐标和颜色数据存储在同一个缓冲区中。虽然有着种种好处,但这样做略显笨重,本例中我们将顶点的坐标和颜色分别存储在不同的两个缓冲区中。

● 顶点数组、颜色数组和索引数组按照图7.36的配置进行了修改(第83、92、101行)。

● 为了程序结构紧凑,定义了函数initArrayBuffer(),封装了缓冲区对象的创建、绑定、数据写入和开启等操作(第116、119、126行)。

在阅读代码时,请留意程序是如何实现上面第2点——构建上图中的数据结构。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/139812.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CorelDraw是什么软件?好用吗

很多人都听过CorelDraw的名字,但不知道CorelDraw是什么样的软件。下面就让小编为大家详细介绍一下。 coreldraw是什么软件 CorelDraw是一款专业的图形设计软件。它的主要功能包括矢量图形和位图的编辑。用户可以利用其矢量图形编辑能力,设计各种图标、Logo等精细图…

java框架-Spring-事务

配置 配置事务管理器方法: Beanpublic PlatformTransactionManager platformTransactionManager(){return new DataSourceTransactionManager();}原理

短信登录功能如何实现?

简介: 在日常生活中我们登录/注册某些网站/APP是通常可以选择 密码登录和手机号登录。 为什么手机号发送后会有验证码返回呢? 网站如何识别我的验证码是否正确? 如果我的个人网站也想要实现短信登录功能,具体该如何实现&#xff1…

Webpack监视文件修改,自动重新打包文件

方法一:使用watch监视文件变化 在终端中输入以下指令: npx webpack --watch 我们使用这种方法监听文件变化时只会监听我们计算机本地的文件变化,在开发场景中我们的项目是要部署到服务器中的,因此这种方式并不推荐。 方法二&…

8款常见的自动化测试开源框架

在如今开源的时代,我们就不要再闭门造车了,热烈的拥抱开源吧!本文针对性能测试、Web UI 测试、API 测试、数据库测试、接口测试、单元测试等方面,为大家整理了github或码云上优秀的自动化测试开源项目,希望能给大家带来…

Python_it_heima

P63 list P68 元组 注意:元组内部嵌套的list包含的内容可以修改,但list本身不能修改。 P69 字符串 P71 数据容器(序列)的切片 P73 集合 P75 字典 字典的常用操作 字典课后练习 P78 类数据容器的总结对比 P79 数据容器的通用操作 不…

useCallBack

React.memo 保证了只有props发生变化时,该组件才会重新渲染 (当然组件内部的state 和 context 变化也会导致组件重新渲染),但咱们只要将咱们的子组件包裹,便可以保证Child组件在props不变的情况下,不会重新…

万字解析30张图带你领略glibc内存管理精髓

最近在逛知乎的时候,看到篇帖子,如下: 看了下面所有的回答,要么是没有回答到点上,要么是回答不够深入,所以,借助本文,深入讲解C/C内存管理。 1 写在前面 源码分析本身就很枯燥乏味…

007 数据结构_堆——“C”

前言 本文将会向您介绍关于堆Heap的实现 具体步骤 tips:本文具体步骤的顺序并不是源代码的顺序 typedef int HPDataType; typedef struct Heap {HPDataType* _a;int _size;int _capacity; }Heap;初始化 void HeapCreate(Heap* hp, HPDataType* a, int n) {hp-&…

代码随想录算法训练营 动态规划part08

一、单词拆分 139. 单词拆分 - 力扣(LeetCode) 将字符串 s 长度记为 n,wordDict 长度记为 m。为了方便,我们调整字符串 s 以及将要用到的动规数组的下标从 1 开始。 定义 f[i] 为考虑前 i 个字符,能否使用 wordDict 拼…

linux进程杀不死

项目场景: 虚拟机 问题描述 linux进程杀不死 无反应 原因分析: 进程僵死zombie 解决方案: 进proc或者find命令找到进程所在地址 cat status查看进程杀死子进程

多输入多输出 | MATLAB实现CNN-BiGRU卷积双向门控循环单元多输入多输出

多输入多输出 | MATLAB实现CNN-BiGRU卷积双向门控循环单元多输入多输出 目录 多输入多输出 | MATLAB实现CNN-BiGRU卷积双向门控循环单元多输入多输出预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 MATLAB实现CNN-BiGRU卷积双向门控循环单元多输入多输出&#xf…

Otter改造 增加springboot模块和HTTP调用功能

环境搭建 & 打包 环境搭建: 进入 $otter_home/lib 目录执行:bash install.sh 打包: 进入$otter_home目录执行:mvn clean install -Dmaven.test.skip -Denvrelease发布包位置:$otter_home/target 项目背景 阿里…

「大数据-0」虚拟机VMware安装、配置、使用、创建大数据集群教程

目录 一、下载VMware Wworkstation Pro 16 二、安装VMware Wworkstation Pro 16 三、检查与设置VMware的网卡 1. 检查 2. 设置VMware网段 四、在VMware上安装Linux虚拟机 五、对安装好的虚拟机进行设置 1. 打开设置 2. 设置中文 3. 修改字体大小 4. 修改终端字体大小 5. 关闭虚…

十、性能测试之数据库测试

性能测试之数据库测试 一、 数据库分类二、 mysql安装及密码的修改1、安装:数据库的版本 mysql5.7版方法1:直接安装方法2:使用rpm包安装方法3:docker方式安装 2、修改数据库的密码3、创建库4、创建表 三、存储引擎1、InnoDB特点 2…

进入数据结构的世界

数据结构和算法的概述 一、什么是数据结构二、什么是算法三、如何去学习数据结构和算法四、算法的时间复杂度和空间复杂度4.1 算法效率4.2 大O的渐进表示法4.3 时间复杂度4.4 空间复杂度4.5 常见复杂度对比 一、什么是数据结构 数据结构是计算机存储、组织数据的方式。&#x…

java框架-Springboot3-基础特性+核心原理

文章目录 java框架-Springboot3-基础特性核心原理profiles外部化配置生命周期监听事件触发时机事件驱动开发SPISpringboot容器启动过程自定义starter java框架-Springboot3-基础特性核心原理 profiles 外部化配置 生命周期监听 事件触发时机 事件驱动开发 Component public c…

Hana SQL Format 的希望

以前一直吐槽Hana Sql 无法进行代码格式化,没有abap code的pretty print功能,对运维和理解代码来说都很不方便。这个情况在abap cleaner里得到改善。 现阶段他们abap的美化做的比原始的pretty print还要好,我基本已经不用pretty print&#x…

获取文件上次访问时间

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl Java源码 public void testGetFileTime() {try {String string "E://test.txt";File file new File(string);Path path file.toPath();BasicFileAttributes ba…

TypeScript- 对于对象键名(包括函数键值)不确定的接口,可以使用字符串索引的形式

AXIOS树配置项 有一个需求,通过JSON数据,第一层是对应的页面对象(比如是用户页面),第二层是该页面的API请求名(比如用户的增删改查),第三层是该API的配置信息(比如&…