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

news2025/4/16 18:49:36

目录

前言 

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

通过顶点索引绘制物体

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_MvpMatrix
  gl.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------v3
  var 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 Red
     1.0, -1.0,  1.0,     1.0,  1.0,  0.0,  // v3 Yellow
     1.0, -1.0, -1.0,     0.0,  1.0,  0.0,  // v4 Green
     1.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,    // front
    0, 3, 4,   0, 4, 5,    // right
    0, 5, 6,   0, 6, 1,    // up
    1, 6, 7,   1, 7, 2,    // left
    7, 4, 3,   7, 3, 2,    // down
    4, 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.coloradmin.cn/o/1016177.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RFID产线自动化升级改造管理方案

应用背景 在现代制造业中,产线管理是实现高效生产和优质产品的关键环节,产线管理涉及到生产过程的监控、物料管理、工艺控制、质量追溯等多个方面,有效的产线管理可以提高生产效率、降低成本、改善产品质量,并满足市场需求的变化…

elasticsearch1

个人名片: 博主:酒徒ᝰ. 个人简介:沉醉在酒中,借着一股酒劲,去拼搏一个未来。 本篇励志:三人行,必有我师焉。 本项目基于B站黑马程序员Java《SpringCloud微服务技术栈》,SpringCloud…

iText实战--在现有PDF上工作

6.1 使用PdfReader读取PDF 检索文档和页面信息 D:/data/iText/inAction/chapter03/image_direct.pdf Number of pages: 1 Size of page 1: [0.0,0.0,283.0,416.0] Rotation of page 1: 0 Page size with rotation of page 1: Rectangle: 283.0x416.0 (rot: 0 degrees) Is reb…

重新认识架构—不只是软件设计

前言 什么是架构? 通常情况下,人们对架构的认知仅限于在软件工程中的定义:架构主要指软件系统的结构设计,比如常见的SOLID准则、DDD架构。一个良好的软件架构可以帮助团队更有效地进行软件开发,降低维护成本&#xff0…

C# 查找迷宫路径

1.导入图像&#xff0c;并且将图像转灰度 using var img new Image<Bgr, byte>(_path); using var grayImg img.Convert<Gray, byte>(); 2.自动二值化图像 using var inputGrayOut new Image<Gray, byte>(grayImg.Size); // 计算OTSU阈值 var threshol…

【图像处理】VS编译opencv源码,并调用编译生成的库

背景 有些时候我们需要修改opencv相关源码&#xff0c; 这里介绍怎么编译修改并调用修改后的库文件。 步骤 1、下载相关源码工具&#xff1a; 下载opencv4.8源码并解压 https://down.chinaz.com/soft/40730.htm 下载VS2019&#xff0c;社区版免费 https://visualstudio.micro…

【C++】动态规划题目总结(随做随更)

文章目录 一. 斐波那契数列模型1. 第 N 个泰波那契数2. 三步问题3. 使用最小花费爬楼梯解法一&#xff1a;从左往右填表解法二&#xff1a;从右往左填表 一. 斐波那契数列模型 解题步骤&#xff1a; 确定状态表示&#xff08;最重要&#xff09;&#xff1a;明确dp表里的值所…

用Jmeter进行压测详解

简介&#xff1a; 1.概述 一款工具&#xff0c;功能往往是很多的&#xff0c;细枝末节的地方也很多&#xff0c;实际的测试工作中&#xff0c;绝大多数场景会用到的也就是一些核心功能&#xff0c;根本不需要我们事无巨细的去掌握工具的所有功能。所以本文将用带价最小的方式讲…

RocketMQ的介绍和环境搭建

一、介绍 我也不知道是啥&#xff0c;知道有什么用、怎么用就行了&#xff0c;说到mq&#xff08;MessageQueue&#xff09;就是消息队列&#xff0c;队列是先进先出的一种数据结构&#xff0c;但是RocketMQ不一定是这样&#xff0c;简单的理解一下&#xff0c;就是临时存储的…

功率谱密度PSD(笔记)

能量守恒&#xff1a;时域能量等于频域能量 功率谱密度能够反映随机振动的功率关于频率的分布密度 功率谱密度&#xff08;PSD&#xff09;&#xff1a;单位是功率/Hz。针对功率有限信号(能量有限信号用能量谱密度)。表现为是单位频带内信号功率随频率的变换情况&#xff0c;…

【深度学习】Pytorch 系列教程(十一):PyTorch数据结构:3、变量(Variable)介绍

目录 一、前言 二、实验环境 三、PyTorch数据结构 0、分类 1、张量&#xff08;Tensor&#xff09; 2、张量操作&#xff08;Tensor Operations&#xff09; 3、变量&#xff08;Variable&#xff09; 一、前言 ChatGPT&#xff1a; PyTorch是一个开源的机器学习框架&am…

Vulnhub系列靶机---Deathnote: 1死亡笔记

文章目录 信息收集主机发现端口扫描目录扫描dirsearchgobusterdirb扫描 漏洞利用wpscan扫描Hydra爆破 总结 靶机文档&#xff1a;Deathnote: 1 下载地址&#xff1a;Download (Mirror) 难易程度&#xff1a;so Easy 信息收集 主机发现 端口扫描 访问靶机的80端口&#xff0c;报…

什么是 Microsoft Office 365? Excel on Cloud 的好处

什么是Office 365 Office 365 是 Microsoft 的一套程序&#xff0c;可以在本地运行&#xff0c;也可以同步到云存储。 可以从访问程序。 借助 Office 365&#xff0c;您可以在任何地方进行工作&#xff0c;并与世界各地的同事共享工作文档。 Office 365 支持的设备&#xff1a…

涵盖Java核心知识的综合指南:JavaGuide | 开源日报 0912

Snailclimb/JavaGuide Stars: 133.8k License: Apache-2.0 这是一份涵盖大部分 Java 程序员所需要掌握的核心知识库。该项目包含了 Java 基础、集合、IO、并发等方面的内容&#xff0c;并提供了重要知识点详解和源码分析。此外还有计算机基础&#xff08;操作系统、网络&…

题目 1057: 二级C语言-分段函数

有一个函数如下&#xff0c;写一程序&#xff0c;输入x&#xff0c;输出y值。 保留两位小数 样例输入 1 样例输出 1.00 这道题的思路很简单&#xff0c;我直接用if判断输入的X对应的函数Y的区间&#xff0c;代入对应的函数&#xff0c;求出结果。记得变量用浮点型&#xff…

【异常错误】detected dubious ownership in repository ****** is owned by: ‘

今天在github git的时候&#xff0c;突然出现了这种问题&#xff0c;下面的框出的部分一直显示&#xff1a; detected dubious ownership in repository at D:/Pycharm_workspace/SBDD/1/FLAG D:/Pycharm_workspace/SBDD/1/FLAG is owned by: S-1-5-32-544 but the current use…

Arm发布 Neoverse V2 和 E2:下一代 Arm 服务器 CPU 内核

9月14日&#xff0c;Arm发布了新的处理器内核&#xff1a;V2和E2&#xff0c;在官网已经可以看到相关的TRM 手册了。。 四年前&#xff0c;Arm发布了Neoverse系列的CPU设计。Arm决定加大力度进军服务器和边缘计算市场&#xff0c;专门为这些市场设计Arm CPU内核&#xff0c;而…

UART 协议

文章目录 硬件拓扑基本原理起始位数据帧奇偶校验位停止位 参考 硬件拓扑 在 UART 通信中&#xff0c;两个 UART 直接相互通信。发送 UART 将控制设备&#xff08;如 CPU&#xff09;的并行数据转换为串行形式&#xff0c;以串行方式将其发送到接收 UART。只需要两条线即可在两…

elementUI elfrom表单验证无效、不起作用常见原因

今天遇到一个变态的问题&#xff0c;因页面比较复杂&#xff0c;出现几组条件判断&#xff0c;每个template内部又包含很多表单&#xff01;&#xff01; <template v-if"transformTypeValue 1"></template><template v-else-if"transformTypeV…

项目知识点总结-分页(三)

后端分页查询接口&#xff1a; Controller Service&#xff1a; Mapper&#xff1a; //分页搜索会议的方法List<SearchMeeting> getAllSearchMeeting(Param("sm") SearchMeeting searchMeeting,Param("page") Integer page,Param("pageSize&q…