可视化学习:如何用WebGL绘制3D物体

news2024/9/27 5:54:35

在之前的文章中,我们使用WebGL绘制了很多二维的图形和图像,在学习2D绘图的时候,我们提过很多次关于GPU的高效渲染,但是2D图形的绘制只展示了WebGL部分的能力,WebGL更强大的地方在于,它可以绘制各种3D图形,而3D图形能够极大地增强可视化的表现能力。

相信很多小伙伴都对此有所耳闻,也有不少人学习WebGL,就是冲着它的3D绘图能力。

接下来,我就用一个简单的正立方体的例子来演示在WebGL中如何绘制3D物体。

从二维到三维

首先,我们先来绘制一个熟悉的2D图形,正方形。

// vertex
attribute vec2 a_vertexPosition;
attribute vec4 color;

varying vec4 vColor;

void main() {
  gl_PointSize = 1.0;
  vColor = color;
  gl_Position = vec4(a_vertexPosition, 1, 1);
}

// fragment
#ifdef GL_ES
precision highp float;
#endif

varying vec4 vColor;

void main() {
  gl_FragColor = vColor;
}

// ...
renderer.setMeshData([{
  positions: [
    [-0.5, -0.5],
    [-0.5, 0.5],
    [0.5, 0.5],
    [0.5, -0.5]
  ],
  attributes: {
    color: [
      [1, 0, 0, 1],
      [1, 0, 0, 1],
      [1, 0, 0, 1],
      [1, 0, 0, 1],
    ]
  },
  cells: [[0, 1, 2], [2, 0, 3]]
}]);
// ...

上述这些代码比较简单,我就不过多解释了。

在画布上我们看到,绘制了一个红色的正方形,它是一个平面图形。

接下来,我们就在这个图形的基础上,将它拓展为3D的正立方体。

要想把2维图形拓展为3维几何体,第一步就是要把顶点扩展到3维。也就是把vec2扩展为vec3。

// vertex
attribute vec3 a_vertexPosition;
attribute vec4 color;

varying vec4 vColor;

void main() {
  gl_PointSize = 1.0;
  vColor = color;
  gl_Position = vec4(a_vertexPosition, 1);
}

当然仅仅修改Shader是不够的,因为数据是从JavaScript传递过来的,所以我们需要在JavaScript中计算立方体的顶点数据,然后再传递给Shader。

一个立方体有8个顶点,能组成6个面。在WebGL中需要用12个三角形来绘制它。

如果6个面的属性相同的话,我们可以复用8个顶点来绘制;

但如果属性不完全相同,比如每个面要绘制成不同的颜色,或者添加不同的纹理图片,就得把每个面的顶点分开。这样的话,就需要24个顶点来分别处理6个面。

为了方便使用,我们可以定义一个JavaScript函数,用来生成立方体6个面的24个顶点,以及12个三角形的索引,并且定义每个面的颜色。

/**
 * 生成立方体6个面的24个顶点,12个三角形的索引,定义每个面的颜色信息
 * @param size
 * @param colors
 * @returns {{cells: *[], color: *[], positions: *[]}}
 */
export function cube(size = 1.0, colors = [[1, 0, 0, 1]]) {
    const h = 0.5 * size;
    const vertices = [
        [-h, -h, -h],
        [-h, h, -h],
        [h, h, -h],
        [h, -h, -h],
        [-h, -h, h],
        [-h, h, h],
        [h, h, h],
        [h, -h, h]
    ];

    const positions = [];
    const color = [];
    const cells = [];

    let colorIdx = 0;
    let cellsIdx = 0;
    const colorLen = colors.length;

    function quad(a, b, c, d) {
        [a, b, c, d].forEach(item => {
            positions.push(vertices[item]);
            color.push(colors[colorIdx % colorLen]);
        });
        cells.push(
            [0, 1, 2].map(i => i + cellsIdx),
            [0, 2, 3].map(i => i + cellsIdx)
        );
        colorIdx ++;
        cellsIdx += 4;
    }

    quad(1, 0, 3, 2); // 内
    quad(4, 5, 6, 7); // 外
    quad(2, 3, 7, 6); // 右
    quad(5, 4, 0, 1); // 左
    quad(3, 0, 4, 7); // 下
    quad(6, 5, 1, 2); // 上

    return {positions, color, cells};
}

现在我们就可以通过调用cube这个函数,构建出立方体的顶点信息。

const geometry = cube(1.0, [
    [1, 0, 0, 1],   // 红
    [0, 0.5, 0, 1], // 绿
    [0, 0, 1, 1]    // 蓝
]);

通过这段代码,我们就能创建出一个棱长为1的立方体,并且六个面的颜色分别是“红、绿、蓝、红、绿、蓝”。

接下来我们就要把这个立方体的顶点信息传递给Shader。

在传递数据之前,我们需要先了解一个知识点,是关于绘制3D图形与2D图形存在的一点不同,那就是绘制3D图形时,必须要开启深度检测和启用深度缓冲区。

在WebGL中,我们可以通过gl.enable(gl.DEPTH_TEST);这段代码来开启深度检测;在清空画布的时候,也要用gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);这段代码来同时清空颜色缓冲区和深度缓冲区。

启动和清空深度检测和深度缓冲区这两个步骤,非常重要。但是一般情况下,我们几乎不会用原生的方式来编写代码,所以了解一下即可。为了方便使用,在本文演示的例子中,我们还是直接使用gl-renderer这个库,它封装了深度检测,我们在使用时,在创建renderer的时候配置一个参数depth: true就可以了。

现在我们就把这个三维立方体用gl-renderer渲染出来。

// ...
renderer = new GlRenderer(glRef.value, {
  depth: true // 开启深度检测
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);
renderer.setMeshData([{
  positions: geometry.positions,
  attributes: {
    color: geometry.color
  },
  cells: geometry.cells
}]);
renderer.render();

现在我们在画布上看到的是一个红色正方形,这是因为其他面被遮挡住了。

投影矩阵:变换WebGL坐标系

但是,等等,为什么我们看到的是红色的一面呢?按照我们所编写的代码,预期看到的应该是绿色的一面,也就是说我们预期Z轴是向外的,因为规范的直角坐标系是右手坐标系。所以按照现在的绘制结果,我们发现WebGL的坐标系其实是左手系的?

但一般来说,不管什么图形库或者图形框架,在绘图的时候,都会默认将坐标系从左手系转换为右手系,因为这更符合我们的使用习惯。所以这里,我们也去把WebGL的坐标系从左手系转换为右手系,简单来说,就是将Z轴坐标方向反转。关于坐标转换,可以通过齐次矩阵来完成。对坐标转换不熟悉的小伙伴,可以参考我之前的一篇关于仿射变换的文章。

将Z轴坐标方向反转,对应的齐次矩阵是这样的:

[
	1, 0, 0, 0,
	0, 1, 0, 0,
	0, 0, -1, 0,
	0, 0, 0, 1
]

这种转换坐标的齐次矩阵,也被称为投影矩阵,ProjectionMatrix。

现在我们修改一下顶点着色器,把这个投影矩阵添加进去。

// vertex
attribute vec3 a_vertexPosition; // 1:把顶点从vec2扩展到vec3
attribute vec4 color; // 四维向量

varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩阵-变换坐标系

void main() {
  gl_PointSize = 1.0;
  vColor = color;
  gl_Position = projectionMatrix * vec4(a_vertexPosition, 1.0);
}

现在我们就能看到画布上显示的是绿色的正方形了。

模型矩阵:让立方体旋转起来

现在我们只能看到立方体的一个面,因为Z轴是垂直于屏幕的,这样子从视觉上看好像和2维图形没什么区别,没法让人很直观地联想、感受到这是一个三维的几何体,为了将其他的面露出来,我们可以去旋转立方体。

要想旋转立方体,我们同样可以通过矩阵运算来实现。这个矩阵叫做模型矩阵,ModelMatrix,它定义了被绘制的物体变换。

把模型矩阵加入到顶点着色器中,将它与投影矩阵相乘,再乘上齐次坐标,就得到最终的顶点坐标了。

attribute vec3 a_vertexPosition; // 1:把顶点从vec2扩展到vec3
attribute vec4 color; // 四维向量

varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩阵-变换坐标系
uniform mat4 modelMatrix; // 3:模型矩阵-使几何体旋转

void main() {
  gl_PointSize = 1.0;
  vColor = color;
  gl_Position = projectionMatrix * modelMatrix * vec4(a_vertexPosition, 1.0);
}

现在我们定义一个JavaScript函数,用立方体沿x、y、z轴的旋转来生成模型矩阵。

以x、y、z三个方向的旋转得到三个齐次矩阵,然后将它们相乘,就能得到最终的模型矩阵。

import { multiply } from 'ogl/src/math/functions/Mat4Func.js';
// ...
export function fromRotation(rotationX, rotationY, rotationZ) {
    let c = Math.cos(rotationX);
    let s = Math.sin(rotationX);
    const rx = [
        1,  0, 0, 0, // 绕X轴旋转
        0,  c, s, 0,
        0, -s, c, 0,
        0,  0, 0, 1
    ];

    c = Math.cos(rotationY);
    s = Math.sin(rotationY);
    const ry = [
        c,  0, s, 0,
        0,  1, 0, 0, // 绕Y轴旋转
        -s, 0, c, 0,
        0,  0, 0, 1
    ];

    c = Math.cos(rotationZ);
    s = Math.sin(rotationZ);
    const rz = [
        c,  s, 0, 0,
        -s, c, 0, 0,
        0,  0, 1, 0, // 绕Z轴旋转
        0,  0, 0, 1
    ];

    const ret = [];
    multiply(ret, rx, ry);
    multiply(ret, ret, rz);
    return ret;
}

我们把模型矩阵传给顶点着色器,不断更新三个旋转角度,就能实现立方体旋转的效果。

// ...
let rotationX = 0;
let rotationY = 0;
let rotationZ = 0;
function update() {
  rotationX += 0.003;
  rotationY += 0.005;
  rotationZ += 0.007;
  renderer.uniforms.modelMatrix = fromRotation(rotationX, rotationY, rotationZ);
  requestAnimationFrame(update);
}
update();
// ...

现在我们就能在旋转中看到立方体的其他几个面了,能更直观地感受到这是一个三维物体。

总结

至此,我们就实现了正立方体的绘制。在3D物体的绘制中,正立方体属于是比较简单的一类,屏幕前的小伙伴们都可以来动手尝试下,感兴趣的小伙伴,还可以尝试去实现圆柱体、正四面体等等这些几何体的绘制。

文章转载自:beckyye

原文链接:https://www.cnblogs.com/beckyyyy/p/18293794

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

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

相关文章

一行命令快速导出、导入Python的依赖环境(Python)

文章目录 一、pip1、导出2、导入 二、Conda(简)1、导出1、导入 一、pip 1、导出 在Pycharm的Terminal窗口输入如下命令,即可将环境导出至文件requirements.txt。 pip freeze > C:\Users\sdl\Deskto\requirements.txt也可以在DOS界面执行…

python:sympy 求解一元五次方程式

pip install sympy 或者 本人用的 anaconda 3 自带 sympy 在北大数学训练营,韦东奕 用卡丹公式 巧妙 求解一元五次方程式: \latex $x^510*x^320*x-4 0$ from sympy import *x symbols(x) expr x**5 10*x**3 20*x -4# 用卡丹公式 尝试化简 a sym…

【操作系统】进程管理——用信号量机制解决问题,以生产者-消费者问题为例(个人笔记)

学习日期:2024.7.10 内容摘要:利用信号量机制解决几个经典问题模型 目录 引言 问题模型 生产者-消费者问题(经典) 多生产者-多消费者问题 吸烟者问题 读者写者问题(难点) 哲学家进餐问题&#xff0…

如何在vue的项目中导入阿里巴巴图标库

阿里巴巴矢量图标库官网:iconfont-阿里巴巴矢量图标库 选择你喜欢的图标,添加入库 点击添加至项目,并新建文件夹,点击确定 选择font-class,点击生成代码 代码生成后,在网站上打开 全选复制到style 点击复制…

Agents 要点

一、Agents概念 人类是这个星球上最强大的 Agent。Agent是一个能感知并自主地采取行动的实体,这里的自主性极其关键,Agent要能够实现设定的目标,其中包括具备学习和获取知识的能力以提高自身性能。 关键点:感知环境、自主决策、具…

SpringBoot新手快速入门系列教程十一:基于Docker Compose部署一个最简单分部署服务项目

如果您还对于Docker或者Docker Compose不甚了解,可以劳烦移步到我之前的教程: SpringBoot新手快速入门系列教程九:基于docker容器,部署一个简单的项目 SpringBoot新手快速入门系列教程十:基于Docker Compose&#xf…

CSS特效:pointer-events: none;的一种特殊应用

一、需求描述 今天看到一个设计需求:需要在弹框中显示如下界面,其中有两个效果: 1.顶部点击项目,下面的内容能相应滚动定位,同时滚动的时候顶部项目也能相应激活显示 2.顶部右侧有一个模糊渐变效果,并且要…

day29--452. 用最少数量的箭引爆气球+435. 无重叠区间+763.划分字母区间

一、452. 用最少数量的箭引爆气球 题目链接:https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/ 文章讲解:https://programmercarl.com/0452.%E7%94%A8%E6%9C%80%E5%B0%91%E6%95%B0%E9%87%8F%E7%9A%84%E7%AE%AD%E5%BC%95%E7%88…

ISO/OIS的七层模型②

OSI模型是一个分层的模型,每一个部分称为一层,每一层扮演固定的角色,互不干扰。OSI有7层,从上到下分别是: 一,每层功能 7.应用层(Application layer ):应用层功能&#x…

AI克隆声音,基于函数计算部署GPT-Sovits语音生成模型

阿里云的 https://developer.aliyun.com/adc/scenario/808348a321844a62b922187d89cd5077 还是 函数计算 FC (aliyun.com) 选择 语音克隆生成 GPT-SOVITS 通过访问域名就能访问 就可以上传个人的声音,然后进行输出 。

【第29章】MyBatis-Plus之分页插件

文章目录 前言一、支持的数据库二、配置方法三、属性介绍四、自定义 Mapper 方法中使用分页五、其他注意事项六、Page 类七、实战1. 配置类2. 分页类3. 测试 总结 前言 MyBatis-Plus 的分页插件 PaginationInnerInterceptor 提供了强大的分页功能,支持多种数据库&a…

SpringBoot3+Vue3开发园区管理系统

介绍 在当今快速发展的城市化进程中,高效、智能的园区管理成为了提升居民生活品质、优化企业运营环境的关键。为此,我们精心打造了全方位、一体化的园区综合管理系统,该系统深度融合了园区管理、楼栋管理、楼层管理、房间管理以及车位管理等…

openlayers WebGL裁剪图层,双图层拼接显示

本篇介绍一下使用openlayers WebGL裁剪图层,双图层拼接显示 1 需求 WebGL裁剪图层,双图层拼接显示 2 分析 图层prerender和postrender事件的使用 WebGL scissor方法的使用 scissor方法指定了一个裁剪区域,用来将绘图区域限制在其限定的盒…

深度学习中的超参管理方法:argparse模块

在深度学习方法中我们不可避免地会遇到大量超参数如(batch_size、learning_rate等)。不同的超参数组合可以得到不同的训练/测试结果。所以在训练和测试过程中我们需要不断调整超参数获得理想的结果(炼丹),如果每一次去…

设备管理中的数据结构

一、有哪些数据结构属于设备管理数据结构 1. 设备控制表(DCT) “Device Control Table”的首字母缩写 2. 控制器控制表(COCT) “Controller Of Control Table”的首字母缩写。 3. 通道控制表(CHCT) “…

简单实现一个本地ChatGPT web服务(langchain框架)

简单实现一个本地ChatGPT 服务,用到langchain框架,fastapi,并且本地安装了ollama。 依赖安装: pip install langchain pip install langchain_community pip install langchain-cli # langchain v0.2 2024年5月最新版本 pip install bs4 pi…

基于swagger插件的方式推送接口文档至torna

目录 一、前言二、登录torna三、创建/选择空间四、创建/选择项目五、创建/选择应用六、获取应用的token七、服务推送7.1 引入maven依赖7.2 test下面按照如下方式新建文件 一、前言 Torna作为一款企业级文档管理系统,支持了很多种接口文档的推送方式。官方比较推荐的…

基于EMQX+Flask+InfluxDB+Grafana打造多协议物联网云平台:MQTT/HTTP设备接入与数据可视化流程(附代码示例)

摘要: 本文深入浅出地介绍了物联网、云平台、MQTT、HTTP、数据可视化等核心概念,并结合 EMQX、Flask、InfluxDB、Grafana 等主流工具,手把手教你搭建一个支持多协议的物联网云平台。文章结构清晰,图文并茂,代码翔实易懂&#xff0…

Linux 入门教程 by 程序员鱼皮

本文作者:程序员鱼皮 免费编程学习 - 编程导航网:https://www.code-nav.cn 大家好,我是鱼皮。 前两天我学编程的老弟小阿巴过生日,我问他想要什么礼物。 本来以为他会要什么游戏机、Q 币卡、鼠标键盘啥的,结果小阿巴…

网关、DHCP协议、ip地址、子网掩码简单介绍

参考文章:https://baike.baidu.com/item/%E7%BD%91%E5%85%B3/98992?frge_ala https://baike.baidu.com/item/DHCP%E6%9C%8D%E5%8A%A1%E5%99%A8/9956953?fromModulelemma_inlink https://blog.csdn.net/weixin_58783105/article/details/135041342 https://blog.cs…